mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
17 Commits
v3.14.0-be
...
v3.15.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b62d723c | ||
|
|
457952bb5a | ||
|
|
17b789822d | ||
|
|
fd12194c0c | ||
|
|
1c662e9ddc | ||
|
|
968903d227 | ||
|
|
3dedf6c26c | ||
|
|
d1dbfe1023 | ||
|
|
63171a0271 | ||
|
|
872e180339 | ||
|
|
a7709df796 | ||
|
|
dbe283f0fa | ||
|
|
727426bbec | ||
|
|
5a3ee47337 | ||
|
|
8dd8f7c48a | ||
|
|
072c59d85a | ||
|
|
d0e8147a48 |
6
.github/labeler.yml
vendored
6
.github/labeler.yml
vendored
@@ -1,5 +1,6 @@
|
||||
test:
|
||||
- test/**/*
|
||||
- types/tests/**/*
|
||||
|
||||
documentation:
|
||||
- www/**/*
|
||||
@@ -32,4 +33,7 @@ client:
|
||||
|
||||
pages:
|
||||
- src/server/pages/**/*
|
||||
- www/docs/configuration/pages.md
|
||||
- www/docs/configuration/pages.md
|
||||
|
||||
TypeScript:
|
||||
- types/**/*
|
||||
|
||||
@@ -42,7 +42,7 @@ npm i
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
|
||||
|
||||
1. Start the dev application/server and CSS watching:
|
||||
1. Start the dev application/server:
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
46
package-lock.json
generated
46
package-lock.json
generated
@@ -4332,6 +4332,12 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -4692,11 +4698,6 @@
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
|
||||
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
@@ -13465,8 +13466,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"bundled": true,
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
@@ -14510,6 +14512,12 @@
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
|
||||
@@ -17829,11 +17837,6 @@
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
|
||||
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
@@ -18665,10 +18668,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"dev": true
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
|
||||
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
@@ -18835,6 +18837,12 @@
|
||||
"strip-ansi": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
|
||||
@@ -18964,6 +18972,12 @@
|
||||
"strip-ansi": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
|
||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "14.2.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "babel --config-file ./config/babel.config.json src --out-dir dist && node ./config/build.js",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev": "next | npm run watch:css",
|
||||
"dev:with-css": "next | npm run watch:css",
|
||||
"dev": "next",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
|
||||
@@ -6,6 +6,27 @@ import Providers from 'next-auth/providers'
|
||||
// const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// cookies: {
|
||||
// csrfToken: {
|
||||
// name: 'next-auth.csrf-token',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// },
|
||||
// pkceCodeVerifier: {
|
||||
// name: 'next-auth.pkce.code_verifier',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
@@ -19,6 +40,11 @@ export default NextAuth({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// protection: ["pkce", "state"],
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: 'pkce'
|
||||
}),
|
||||
Providers.Twitter({
|
||||
|
||||
@@ -106,7 +106,8 @@ async function getToken (params) {
|
||||
// or not set (e.g. development or test instance) case use unprefixed name
|
||||
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
|
||||
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
|
||||
raw = false
|
||||
raw = false,
|
||||
decode: _decode = decode
|
||||
} = params
|
||||
if (!req) throw new Error('Must pass `req` to JWT getToken()')
|
||||
|
||||
@@ -126,7 +127,7 @@ async function getToken (params) {
|
||||
}
|
||||
|
||||
try {
|
||||
return decode({ token, ...params })
|
||||
return _decode({ token, ...params })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
95
src/server/index.d.ts
vendored
Normal file
95
src/server/index.d.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
import { LoggerInstance } from 'src/lib/logger'
|
||||
import { CallbacksOptions } from './lib/callbacks'
|
||||
import { CookiesOptions } from './lib/cookie'
|
||||
import { EventsOptions } from './lib/events'
|
||||
|
||||
export interface Provider {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
version: string
|
||||
params: Record<string, unknown>
|
||||
scope: string
|
||||
accessTokenUrl: string
|
||||
authorizationUrl: string
|
||||
profileUrl?: string
|
||||
grant_type?: string
|
||||
profile?: (profile: any) => Promise<any>
|
||||
}
|
||||
|
||||
/** @docs https://next-auth.js.org/configuration/options */
|
||||
export interface NextAuthOptions {
|
||||
/** @docs https://next-auth.js.org/configuration/options#theme */
|
||||
theme?: 'auto' | 'dark' | 'light'
|
||||
/** @docs https://next-auth.js.org/configuration/options#providers */
|
||||
providers: Provider[]
|
||||
/** @docs https://next-auth.js.org/configuration/options#database */
|
||||
database?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#secret */
|
||||
secret?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#session */
|
||||
session?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#jwt */
|
||||
jwt?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#pages */
|
||||
pages?: {
|
||||
signIn?: string
|
||||
signOut?: string
|
||||
/** Error code passed in query string as ?error= */
|
||||
error?: string
|
||||
verifyRequest?: string
|
||||
/** If set, new users will be directed here on first sign in */
|
||||
newUser?: string
|
||||
}
|
||||
/**
|
||||
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
|
||||
* Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as
|
||||
* they allow you to implement access controls without a database and
|
||||
* to integrate with external databases or APIs.
|
||||
* @docs https://next-auth.js.org/configuration/options#callbacks
|
||||
*/
|
||||
callbacks?: CallbacksOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#events */
|
||||
events?: EventsOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#adapter */
|
||||
adapter?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#debug */
|
||||
debug?: boolean
|
||||
/** @docs https://next-auth.js.org/configuration/options#usesecurecookies */
|
||||
useSecureCookies?: boolean
|
||||
/** @docs https://next-auth.js.org/configuration/options#cookies */
|
||||
cookies?: CookiesOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#logger */
|
||||
logger: LoggerInstance
|
||||
}
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
export type NextAuthSharedOptions = 'pages' | 'jwt' | 'events' | 'callbacks' | 'cookies' | 'secret' | 'adapter' | 'theme' | 'debug' | 'logger'
|
||||
|
||||
export interface NextAuthInternalOptions extends Pick<NextAuthOptions, NextAuthSharedOptions> {
|
||||
pkce?: {
|
||||
code_verifier?: string
|
||||
/**
|
||||
* Could be `"plain"`, but not recommended.
|
||||
* We ignore it for now.
|
||||
* @spec https://tools.ietf.org/html/rfc7636#section-4.2.
|
||||
*/
|
||||
code_challenge_method?: 'S256'
|
||||
}
|
||||
provider?: Provider
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
action?: string
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
options: NextAuthInternalOptions
|
||||
}
|
||||
|
||||
export interface NextAuthResponse extends NextApiResponse {}
|
||||
|
||||
export declare function NextAuthHandler (req: NextAuthRequest, res: NextAuthResponse, options: NextAuthOptions): ReturnType<NextApiHandler>
|
||||
export declare function NextAuthHandler (options: NextAuthOptions): ReturnType<NextApiHandler>
|
||||
@@ -6,12 +6,12 @@ import * as cookie from './lib/cookie'
|
||||
import * as defaultEvents from './lib/default-events'
|
||||
import * as defaultCallbacks from './lib/default-callbacks'
|
||||
import parseProviders from './lib/providers'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import extendRes from './lib/extend-req'
|
||||
import * as routes from './routes'
|
||||
import renderPage from './pages'
|
||||
import csrfTokenHandler from './lib/csrf-token-handler'
|
||||
import createSecret from './lib/create-secret'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import extendRes from './lib/extend-res'
|
||||
import csrfTokenHandler from './lib/csrf-token-handler'
|
||||
import * as pkce from './lib/oauth/pkce-handler'
|
||||
import * as state from './lib/oauth/state-handler'
|
||||
|
||||
@@ -67,16 +67,18 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
|
||||
const secret = createSecret({ userOptions, basePath, baseUrl })
|
||||
|
||||
const { csrfToken, csrfTokenVerified } = csrfTokenHandler(req, res, cookies, secret)
|
||||
|
||||
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
if (provider &&
|
||||
provider.type === 'oauth' && provider.version?.startsWith('2') &&
|
||||
(!provider.protection && provider.state !== false)
|
||||
) {
|
||||
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
|
||||
// Protection only works on OAuth 2.x providers
|
||||
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
|
||||
// When provider.state is undefined, we still want this to pass
|
||||
if (!provider.protection && provider.state !== false) {
|
||||
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ['state']
|
||||
} else if (typeof provider.protection === 'string') {
|
||||
provider.protection = [provider.protection]
|
||||
}
|
||||
}
|
||||
|
||||
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
|
||||
@@ -103,7 +105,6 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
provider,
|
||||
cookies,
|
||||
secret,
|
||||
csrfToken,
|
||||
providers,
|
||||
// Session options
|
||||
session: {
|
||||
@@ -134,6 +135,7 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
logger
|
||||
}
|
||||
|
||||
csrfTokenHandler(req, res)
|
||||
await callbackUrlHandler(req, res)
|
||||
|
||||
const render = renderPage(req, res)
|
||||
@@ -146,7 +148,7 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
case 'session':
|
||||
return routes.session(req, res)
|
||||
case 'csrf':
|
||||
return res.json({ csrfToken })
|
||||
return res.json({ csrfToken: req.options.csrfToken })
|
||||
case 'signin':
|
||||
if (pages.signIn) {
|
||||
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
|
||||
@@ -199,7 +201,7 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
switch (action) {
|
||||
case 'signin':
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (csrfTokenVerified && provider) {
|
||||
if (req.options.csrfTokenVerified && provider) {
|
||||
if (await pkce.handleSignin(req, res)) return
|
||||
if (await state.handleSignin(req, res)) return
|
||||
return routes.signin(req, res)
|
||||
@@ -208,14 +210,14 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
case 'signout':
|
||||
// Verified CSRF Token required for signout
|
||||
if (csrfTokenVerified) {
|
||||
if (req.options.csrfTokenVerified) {
|
||||
return routes.signout(req, res)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
case 'callback':
|
||||
if (provider) {
|
||||
// Verified CSRF Token required for credentials providers only
|
||||
if (provider.type === 'credentials' && !csrfTokenVerified) {
|
||||
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,29 +14,30 @@ import * as cookie from './cookie'
|
||||
* For more details, see the following OWASP links:
|
||||
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
|
||||
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
|
||||
* @param {import("..").NextAuthRequest} req
|
||||
* @param {import("..").NextAuthResponse} res
|
||||
*/
|
||||
export default function csrfTokenHandler (req, res, cookies, secret) {
|
||||
const { csrfToken: csrfTokenFromRequest } = req.body
|
||||
|
||||
let csrfTokenFromCookie
|
||||
let csrfTokenVerified = false
|
||||
if (req.cookies[cookies.csrfToken.name]) {
|
||||
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
|
||||
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
|
||||
export default function csrfTokenHandler (req, res) {
|
||||
const { cookies, secret } = req.options
|
||||
if (cookies.csrfToken.name in req.cookies) {
|
||||
const [csrfToken, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
|
||||
const expectedCsrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
|
||||
if (csrfTokenHash === expectedCsrfTokenHash) {
|
||||
// If hash matches then we trust the CSRF token value
|
||||
csrfTokenFromCookie = csrfTokenValue
|
||||
|
||||
// If this is a POST request and the CSRF Token in the Post request matches
|
||||
// the cookie we have already verified is one we have set, then token is verified!
|
||||
if (req.method === 'POST' && csrfTokenFromCookie === csrfTokenFromRequest) { csrfTokenVerified = true }
|
||||
// If this is a POST request and the CSRF Token in the POST request matches
|
||||
// the cookie we have already verified is the one we have set, then the token is verified!
|
||||
const csrfTokenVerified = req.method === 'POST' && csrfToken === req.body.csrfToken
|
||||
req.options.csrfToken = csrfToken
|
||||
req.options.csrfTokenVerified = csrfTokenVerified
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!csrfTokenFromCookie) {
|
||||
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
|
||||
// (e.g. because it's been modifed or because the secret has changed) create a new token.
|
||||
csrfTokenFromCookie = randomBytes(32).toString('hex')
|
||||
const newCsrfTokenCookie = `${csrfTokenFromCookie}|${createHash('sha256').update(`${csrfTokenFromCookie}${secret}`).digest('hex')}`
|
||||
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
|
||||
}
|
||||
return { csrfToken: csrfTokenFromCookie, csrfTokenVerified }
|
||||
// If no csrfToken from cookie - because it's not been set yet,
|
||||
// or because the hash doesn't match (e.g. because it's been modifed or because the secret has changed)
|
||||
// create a new token.
|
||||
const csrfToken = randomBytes(32).toString('hex')
|
||||
const csrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
|
||||
const csrfTokenCookie = `${csrfToken}|${csrfTokenHash}`
|
||||
cookie.set(res, cookies.csrfToken.name, csrfTokenCookie, cookies.csrfToken.options)
|
||||
req.options.csrfToken = csrfToken
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { decode as jwtDecode } from 'jsonwebtoken'
|
||||
import oAuthClient from './client'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
import { decode as jwtDecode } from "jsonwebtoken"
|
||||
import oAuthClient from "./client"
|
||||
import logger from "../../../lib/logger"
|
||||
import { OAuthCallbackError } from "../../../lib/errors"
|
||||
|
||||
/** @param {import("types/internals").NextAuthRequest} req */
|
||||
export default async function oAuthCallback (req) {
|
||||
export default async function oAuthCallback(req) {
|
||||
const { provider, pkce } = req.options
|
||||
const client = oAuthClient(provider)
|
||||
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
if (provider.version?.startsWith("2.")) {
|
||||
// The "user" object is specific to the Apple provider and is provided on first sign in
|
||||
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
|
||||
let { code, user } = req.query // eslint-disable-line camelcase
|
||||
|
||||
if (req.method === 'POST') {
|
||||
if (req.method === "POST") {
|
||||
try {
|
||||
const body = JSON.parse(JSON.stringify(req.body))
|
||||
if (body.error) {
|
||||
@@ -23,25 +23,35 @@ export default async function oAuthCallback (req) {
|
||||
code = body.code
|
||||
user = body.user != null ? JSON.parse(body.user) : null
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
|
||||
logger.error(
|
||||
"OAUTH_CALLBACK_HANDLER_ERROR",
|
||||
error,
|
||||
req.body,
|
||||
provider.id,
|
||||
code
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// REVIEW: Is this used by any of the providers?
|
||||
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
|
||||
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
|
||||
if (Object.prototype.hasOwnProperty.call(provider, "useAuthTokenHeader")) {
|
||||
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
|
||||
} else {
|
||||
client.useAuthorizationHeaderforGET(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await client.getOAuthAccessToken(code, provider, pkce.code_verifier)
|
||||
const tokens = await client.getOAuthAccessToken(
|
||||
code,
|
||||
provider,
|
||||
pkce.code_verifier
|
||||
)
|
||||
let profileData
|
||||
if (provider.idToken) {
|
||||
if (!tokens?.id_token) {
|
||||
throw new OAuthCallbackError('Missing JWT ID Token')
|
||||
throw new OAuthCallbackError("Missing JWT ID Token")
|
||||
}
|
||||
|
||||
// Support services that use OpenID ID Tokens to encode profile data
|
||||
@@ -52,26 +62,28 @@ export default async function oAuthCallback (req) {
|
||||
|
||||
return getProfile({ profileData, provider, tokens, user })
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
|
||||
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id, code)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle OAuth v1.x
|
||||
const {
|
||||
oauth_token: oauthToken, oauth_verifier: oauthVerifier
|
||||
} = req.query
|
||||
const tokens = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
|
||||
// eslint-disable-next-line camelcase
|
||||
const { oauth_token, oauth_verifier } = req.query
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const { token_secret } = await client.getOAuthRequestToken(provider.params)
|
||||
const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier)
|
||||
const profileData = await client.get(
|
||||
provider.profileUrl,
|
||||
tokens.accessToken,
|
||||
tokens.refreshToken
|
||||
tokens.oauth_token,
|
||||
tokens.oauth_token_secret
|
||||
)
|
||||
|
||||
return getProfile({ profileData, tokens, provider })
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
|
||||
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -81,15 +93,27 @@ export default async function oAuthCallback (req) {
|
||||
* Returns profile, raw profile and auth provider details
|
||||
* @param {{
|
||||
* profileData: object | string
|
||||
* tokens: import("types").TokenSet
|
||||
* provider: import("types/providers").OAuthConfig
|
||||
* tokens: {
|
||||
* accessToken: string
|
||||
* idToken?: string
|
||||
* refreshToken?: string
|
||||
* access_token: string
|
||||
* expires_in?: string | Date | null
|
||||
* refresh_token?: string
|
||||
* id_token?: string
|
||||
* token?: string
|
||||
* token_secret?: string
|
||||
* tokenSecret?: string
|
||||
* params?: any
|
||||
* }
|
||||
* provider: import("../..").Provider
|
||||
* user?: object
|
||||
* }} profileParams
|
||||
*/
|
||||
async function getProfile ({ profileData, tokens, provider, user }) {
|
||||
async function getProfile({ profileData, tokens, provider, user }) {
|
||||
try {
|
||||
// Convert profileData into an object if it's a string
|
||||
if (typeof profileData === 'string' || profileData instanceof String) {
|
||||
if (typeof profileData === "string" || profileData instanceof String) {
|
||||
profileData = JSON.parse(profileData)
|
||||
}
|
||||
|
||||
@@ -98,22 +122,22 @@ async function getProfile ({ profileData, tokens, provider, user }) {
|
||||
profileData.user = user
|
||||
}
|
||||
|
||||
logger.debug('PROFILE_DATA', profileData)
|
||||
logger.debug("PROFILE_DATA", profileData)
|
||||
|
||||
const profile = await provider.profile(profileData, tokens)
|
||||
// Return profile, raw profile and auth provider details
|
||||
return {
|
||||
profile: {
|
||||
...profile,
|
||||
email: profile.email?.toLowerCase() ?? null
|
||||
email: profile.email?.toLowerCase() ?? null,
|
||||
},
|
||||
account: {
|
||||
provider: provider.id,
|
||||
type: provider.type,
|
||||
id: profile.id,
|
||||
...tokens
|
||||
...tokens,
|
||||
},
|
||||
OAuthProfile: profileData
|
||||
OAuthProfile: profileData,
|
||||
}
|
||||
} catch (exception) {
|
||||
// If we didn't get a response either there was a problem with the provider
|
||||
@@ -123,11 +147,11 @@ async function getProfile ({ profileData, tokens, provider, user }) {
|
||||
// all providers, so we return an empty object; the user should then be
|
||||
// redirected back to the sign up page. We log the error to help developers
|
||||
// who might be trying to debug this when configuring a new provider.
|
||||
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData)
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData)
|
||||
return {
|
||||
profile: null,
|
||||
account: null,
|
||||
OAuthProfile: profileData
|
||||
OAuthProfile: profileData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,23 +54,36 @@ export default function oAuthClient (provider) {
|
||||
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthAccessToken = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve({ accessToken, refreshToken, results })
|
||||
|
||||
resolve({
|
||||
// TODO: Remove, this is only kept for backward compativility
|
||||
// These are not in the OAuth 1.x spec
|
||||
accessToken: oauth_token,
|
||||
refreshToken: oauth_token_secret,
|
||||
results: params,
|
||||
|
||||
oauth_token,
|
||||
oauth_token_secret,
|
||||
params
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthRequestToken = (...args) => {
|
||||
oauth1Client.getOAuthRequestToken = (params = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
originalGetOAuthRequestToken(...args, (error, oauthToken) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve(oauthToken)
|
||||
resolve({ oauth_token, oauth_token_secret, params })
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -136,7 +149,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
headers.Authorization = `Bearer ${code}`
|
||||
}
|
||||
|
||||
if (provider.protection === 'pkce') {
|
||||
if (provider.protection.includes('pkce')) {
|
||||
params.code_verifier = codeVerifier
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
export async function handleCallback (req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
|
||||
// Provider does not support PKCE, nothing to do.
|
||||
if (!provider.protection?.includes('pkce')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ export async function handleCallback (req, res) {
|
||||
export async function handleSignin (req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
|
||||
if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do.
|
||||
return
|
||||
}
|
||||
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie
|
||||
|
||||
@@ -12,11 +12,12 @@ import { OAuthCallbackError } from '../../../lib/errors'
|
||||
export async function handleCallback (req, res) {
|
||||
const { csrfToken, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
|
||||
// Provider does not support state, nothing to do.
|
||||
if (!provider.protection?.includes('state')) {
|
||||
return
|
||||
}
|
||||
|
||||
const { state } = req.query
|
||||
const state = req.query.state || req.body.state
|
||||
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
|
||||
|
||||
logger.debug(
|
||||
@@ -41,7 +42,7 @@ export async function handleCallback (req, res) {
|
||||
export async function handleSignin (req, res) {
|
||||
const { provider, baseUrl, basePath, csrfToken } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
|
||||
if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,17 @@ import logger from '../../../lib/logger'
|
||||
export default async function getAuthorizationUrl (req) {
|
||||
const { provider } = req.options
|
||||
|
||||
delete req.query?.nextauth
|
||||
const params = {
|
||||
...provider.authorizationParams,
|
||||
...req.query
|
||||
}
|
||||
|
||||
const client = oAuthClient(provider)
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
delete req.query?.nextauth
|
||||
// Handle OAuth v2.x
|
||||
let url = client.getAuthorizeUrl({
|
||||
...provider.authorizationParams,
|
||||
...req.query,
|
||||
...params,
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope
|
||||
})
|
||||
@@ -34,8 +38,12 @@ export default async function getAuthorizationUrl (req) {
|
||||
}
|
||||
|
||||
try {
|
||||
const oAuthToken = await client.getOAuthRequestToken()
|
||||
const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
|
||||
const tokens = await client.getOAuthRequestToken(params)
|
||||
const url = `${provider.authorizationUrl}?${new URLSearchParams({
|
||||
oauth_token: tokens.oauth_token,
|
||||
oauth_token_secret: tokens.oauth_token_secret,
|
||||
...tokens.params
|
||||
})}`
|
||||
logger.debug('GET_AUTHORIZATION_URL', url)
|
||||
return url
|
||||
} catch (error) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function error ({ baseUrl, basePath, error = 'default', res }) {
|
||||
}
|
||||
}
|
||||
|
||||
const { statusCode, heading, message, signin } = errors[error.toLowerCase()]
|
||||
const { statusCode, heading, message, signin } = errors[error.toLowerCase()] ?? errors.default
|
||||
|
||||
res.status(statusCode)
|
||||
|
||||
|
||||
4
types/adapters.d.ts
vendored
4
types/adapters.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
import { AppOptions } from "internals"
|
||||
import { AppOptions } from "./internals"
|
||||
import { ConnectionOptions, EntitySchema } from "typeorm"
|
||||
import { User } from "."
|
||||
import { AppProvider } from "internals/providers"
|
||||
import { AppProvider } from "./internals/providers"
|
||||
|
||||
export interface Profile {
|
||||
id: string
|
||||
|
||||
109
types/index.d.ts
vendored
109
types/index.d.ts
vendored
@@ -11,7 +11,7 @@ import {
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
NextApiHandler,
|
||||
} from "internals/utils"
|
||||
} from "./internals/utils"
|
||||
|
||||
/**
|
||||
* Configure your NextAuth instance
|
||||
@@ -26,7 +26,7 @@ export interface NextAuthOptions {
|
||||
* * **Default value**: `[]`
|
||||
* * **Required**: *Yes*
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentaion](https://next-auth.js.org/configuration/providers)
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
|
||||
*/
|
||||
providers: AppProviders
|
||||
/**
|
||||
@@ -38,9 +38,9 @@ export interface NextAuthOptions {
|
||||
*/
|
||||
database?: string | Record<string, any> | ConnectionOptions
|
||||
/**
|
||||
* A random string used to hash tokens, sign cookies and generate crytographic keys.
|
||||
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
* The default behaviour is volatile, and **it is strongly recommended** you explicitly specify a value
|
||||
* The default behavior is volatile, and **it is strongly recommended** you explicitly specify a value
|
||||
* to avoid invalidating end user sessions when configuration changes are deployed.
|
||||
* * **Default value**: `string` (SHA hash of the "options" object)
|
||||
* * **Required**: No - **but strongly recommended**!
|
||||
@@ -51,7 +51,7 @@ export interface NextAuthOptions {
|
||||
/**
|
||||
* Configure your session like if you want to use JWT or a database,
|
||||
* how long until an idle session expires, or to throttle write operations in case you are using a database.
|
||||
* * **Default value**: See the documentaion page
|
||||
* * **Default value**: See the documentation page
|
||||
* * **Required**: No
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#session)
|
||||
@@ -86,17 +86,17 @@ export interface NextAuthOptions {
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentaion](https://next-auth.js.org/configuration/pages)
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages)
|
||||
*/
|
||||
pages?: PagesOptions
|
||||
/**
|
||||
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
|
||||
* Callbacks are *extremely powerful*, especially in scenarios involving JSON Web Tokens
|
||||
* as they **allow you to implement access controls without a database** and to **integrate with external databases or APIs**.
|
||||
* * **Default value**: See the Callbacks documentaion
|
||||
* * **Default value**: See the Callbacks documentation
|
||||
* * **Required**: *No*
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentaion](https://next-auth.js.org/configuration/callbacks)
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
|
||||
*/
|
||||
callbacks?: CallbacksOptions
|
||||
/**
|
||||
@@ -109,7 +109,7 @@ export interface NextAuthOptions {
|
||||
* * **Default value**: `{}`
|
||||
* * **Required**: *No*
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentaion](https://next-auth.js.org/configuration/events)
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
|
||||
*/
|
||||
events?: EventsOptions
|
||||
/**
|
||||
@@ -135,7 +135,7 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* - ⚠ If you added a custom `logger`, this setting is ignored.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentaion](https://next-auth.js.org/configuration/options#logger)
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentation](https://next-auth.js.org/configuration/options#logger)
|
||||
*/
|
||||
debug?: boolean
|
||||
/**
|
||||
@@ -166,7 +166,8 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* - ⚠ When set, the `debug` option is ignored
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#logger) | [Debug documentaion](https://next-auth.js.org/configuration/options#debug)
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#logger) |
|
||||
* [Debug documentation](https://next-auth.js.org/configuration/options#debug)
|
||||
*/
|
||||
logger?: LoggerInstance
|
||||
/**
|
||||
@@ -182,7 +183,7 @@ export interface NextAuthOptions {
|
||||
theme?: "auto" | "dark" | "light"
|
||||
/**
|
||||
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
|
||||
* This option defaults to `false` on URLs that start with http:// (e.g. http://localhost:3000) for developer convenience.
|
||||
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
|
||||
* You can manually set this option to `false` to disable this security feature and allow cookies
|
||||
* to be accessible from non-secured URLs (this is not recommended).
|
||||
* * **Default value**: `true` for HTTPS and `false` for HTTP sites
|
||||
@@ -199,7 +200,7 @@ export interface NextAuthOptions {
|
||||
* You can override the default cookie names and options for any of the cookies used by NextAuth.js.
|
||||
* You can specify one or more cookies with custom properties,
|
||||
* but if you specify custom options for a cookie you must provide all the options for that cookie.
|
||||
* If you use this feature, you will likely want to create conditional behaviour
|
||||
* If you use this feature, you will likely want to create conditional behavior
|
||||
* to support setting different cookies policies in development and production builds,
|
||||
* as you will be opting out of the built-in dynamic policy.
|
||||
* * **Default value**: `{}`
|
||||
@@ -214,12 +215,22 @@ export interface NextAuthOptions {
|
||||
cookies?: CookiesOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Override any of the methods, and the rest will use the default logger.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#logger)
|
||||
*/
|
||||
export interface LoggerInstance {
|
||||
warn(code: string, ...message: unknown[]): void
|
||||
error(code: string, ...message: unknown[]): void
|
||||
debug(code: string, ...message: unknown[]): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Different tokens returned by OAuth Providers.
|
||||
* Some of them are available with different casing,
|
||||
* but they refer to the same value.
|
||||
*/
|
||||
export interface TokenSet {
|
||||
accessToken: string
|
||||
idToken?: string
|
||||
@@ -230,12 +241,17 @@ export interface TokenSet {
|
||||
id_token?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Usually contains information about the provider being used
|
||||
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
|
||||
*/
|
||||
export interface Account extends TokenSet, Record<string, unknown> {
|
||||
id: string
|
||||
provider: string
|
||||
type: string
|
||||
}
|
||||
|
||||
/** The OAuth profile returned from your provider */
|
||||
export interface Profile extends Record<string, unknown> {
|
||||
sub?: string
|
||||
name?: string
|
||||
@@ -243,13 +259,54 @@ export interface Profile extends Record<string, unknown> {
|
||||
image?: string
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
|
||||
export interface CallbacksOptions<
|
||||
P extends Record<string, unknown> = Profile,
|
||||
A extends Record<string, unknown> = Account
|
||||
> {
|
||||
/**
|
||||
* Use this callback to control if a user is allowed to sign in.
|
||||
* Returning true will continue the sign-in flow.
|
||||
* Throwing an error or returning a string will stop the flow, and redirect the user.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
|
||||
*/
|
||||
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
|
||||
/**
|
||||
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
|
||||
* By default only URLs on the same URL as the site are allowed,
|
||||
* you can use this callback to customise that behaviour.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
|
||||
*/
|
||||
redirect?(url: string, baseUrl: string): Awaitable<string>
|
||||
/**
|
||||
* This callback is called whenever a session is checked.
|
||||
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
|
||||
*
|
||||
* - ⚠ By default, only a subset of the token is returned for increased security.
|
||||
* If you want to make something available you added to the token through the `jwt` callback,
|
||||
* you have to explicitely forward it here to make it available to the client.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#session-callback) |
|
||||
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
|
||||
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
|
||||
*
|
||||
*/
|
||||
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
|
||||
/**
|
||||
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
|
||||
* or updated (i.e whenever a session is accessed in the client).
|
||||
* Its content is forwarded to the `session` callback,
|
||||
* where you can control what should be returned to the client.
|
||||
* Anything else will be kept from your front-end.
|
||||
*
|
||||
* - ⚠ By default the JWT is signed, but not encrypted.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
|
||||
*/
|
||||
jwt?(
|
||||
token: JWT,
|
||||
user?: User,
|
||||
@@ -259,6 +316,7 @@ export interface CallbacksOptions<
|
||||
): Awaitable<JWT>
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
export interface CookieOption {
|
||||
name: string
|
||||
options: {
|
||||
@@ -271,6 +329,7 @@ export interface CookieOption {
|
||||
}
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
export interface CookiesOptions {
|
||||
sessionToken?: CookieOption
|
||||
callbackUrl?: CookieOption
|
||||
@@ -278,6 +337,7 @@ export interface CookiesOptions {
|
||||
pkceCodeVerifier?: CookieOption
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
export type EventType =
|
||||
| "signIn"
|
||||
| "signOut"
|
||||
@@ -287,10 +347,13 @@ export type EventType =
|
||||
| "session"
|
||||
| "error"
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
export type EventCallback = (message: any) => Promise<void>
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
export type EventsOptions = Partial<Record<EventType, EventCallback>>
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/pages) */
|
||||
export interface PagesOptions {
|
||||
signIn?: string
|
||||
signOut?: string
|
||||
@@ -301,18 +364,38 @@ export interface PagesOptions {
|
||||
newUser?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Returned by `useSession`, `getSession`, returned by the `session` callback
|
||||
* and also the shape received as a prop on the `Provider` React Context
|
||||
*
|
||||
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
|
||||
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
|
||||
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
|
||||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
|
||||
*/
|
||||
export interface Session extends Record<string, unknown> {
|
||||
user?: User
|
||||
accessToken?: string
|
||||
expires: string
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
|
||||
export interface SessionOptions {
|
||||
jwt?: boolean
|
||||
maxAge?: number
|
||||
updateAge?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The shape of the returned object in the OAuth providers' `profile` callback,
|
||||
* available in the `jwt` and `session` callbacks,
|
||||
* or the second parameter of the `session` callback, when using a database.
|
||||
*
|
||||
* [`signIn` callback](https://next-auth.js.org/configuration/callbacks#sign-in-callback) |
|
||||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
|
||||
*/
|
||||
export interface User {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
|
||||
5
types/internals/index.d.ts
vendored
5
types/internals/index.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import { NextApiRequest, NextApiResponse } from "internals/utils"
|
||||
import { NextApiRequest, NextApiResponse } from "./utils"
|
||||
import { NextAuthOptions } from ".."
|
||||
import { AppProvider } from "internals/providers"
|
||||
import { AppProvider } from "./providers"
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
export type NextAuthSharedOptions =
|
||||
@@ -40,6 +40,7 @@ export interface AppOptions
|
||||
| "verify-request"
|
||||
| "error"
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
|
||||
2
types/internals/providers.d.ts
vendored
2
types/internals/providers.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { CommonProviderOptions } from "next-auth/providers"
|
||||
import { CommonProviderOptions } from "../providers"
|
||||
|
||||
export interface AppProvider extends CommonProviderOptions {
|
||||
signinUrl: string
|
||||
|
||||
10
types/jwt.d.ts
vendored
10
types/jwt.d.ts
vendored
@@ -1,6 +1,11 @@
|
||||
import { JWT as JoseJWT, JWE } from "jose"
|
||||
import { NextApiRequest } from "internals/utils"
|
||||
import { NextApiRequest } from "./internals/utils"
|
||||
|
||||
/**
|
||||
* Returned by the `jwt` callback and `getToken`, when using JWT sessions
|
||||
*
|
||||
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) | [`getToken`](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken)
|
||||
*/
|
||||
export interface JWT extends Record<string, unknown> {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
@@ -44,9 +49,10 @@ export type GetTokenParams<R extends boolean = false> = {
|
||||
secret?: string
|
||||
} & Omit<JWTDecodeParams, "secret">
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken) */
|
||||
export function getToken<R extends boolean = false>(
|
||||
params?: GetTokenParams<R>
|
||||
): Promise<R extends true ? string : JWT>
|
||||
): Promise<R extends true ? string : JWT | null>
|
||||
|
||||
export interface JWTOptions {
|
||||
secret?: string
|
||||
|
||||
5
types/providers.d.ts
vendored
5
types/providers.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { Profile, TokenSet, User } from "."
|
||||
import { Awaitable } from "internals/utils"
|
||||
import { Awaitable } from "./internals/utils"
|
||||
|
||||
export type ProviderType = "oauth" | "email" | "credentials"
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
|
||||
|
||||
export type OAuthProviderType =
|
||||
| "Apple"
|
||||
| "Attlassian"
|
||||
| "Atlassian"
|
||||
| "Auth0"
|
||||
| "AzureADB2C"
|
||||
| "Basecamp"
|
||||
@@ -144,6 +144,7 @@ export interface EmailConfig extends CommonProviderOptions {
|
||||
provider: EmailConfig
|
||||
}): Awaitable<void>
|
||||
}
|
||||
|
||||
export type EmailProvider = (options: Partial<EmailConfig>) => EmailConfig
|
||||
|
||||
// TODO: Rename to Token provider
|
||||
|
||||
@@ -19,7 +19,7 @@ JWTType.getToken({
|
||||
raw: true,
|
||||
})
|
||||
|
||||
// $ExpectType Promise<JWT>
|
||||
// $ExpectType Promise<JWT | null>
|
||||
JWTType.getToken({
|
||||
req: nextReq,
|
||||
secret: "secret",
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"next-auth/providers": ["./providers"],
|
||||
"next-auth/adapters": ["./adapters"],
|
||||
"next-auth/client": ["./client"],
|
||||
"next-auth/jwt": ["./jwt"],
|
||||
"internals": ["./internals"]
|
||||
"next-auth/jwt": ["./jwt"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ When using NextAuth.js without a database, the user object it will always be a p
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If you only want to allow users who already have accounts in the database to sign in, you can check for the existance of a `user.id` property and reject any sign in attempts from accounts that do not have one.
|
||||
If you only want to allow users who already have accounts in the database to sign in, you can check for the existence of a `user.id` property and reject any sign in attempts from accounts that do not have one.
|
||||
|
||||
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
|
||||
:::
|
||||
|
||||
@@ -121,11 +121,29 @@ By default JSON Web Tokens are signed (JWS) but not encrypted (JWE), as JWT encr
|
||||
jwt: {
|
||||
// A secret to use for key generation - you should set this explicitly
|
||||
// Defaults to NextAuth.js secret if not explicitly specified.
|
||||
// This is used to generate the actual signingKey and produces a warning
|
||||
// message if not defined explicitly.
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
|
||||
// You can generate a signing key using `jose newkey -s 512 -t oct -a HS512`
|
||||
// This gives you direct knowledge of the key used to sign the token so you can use it
|
||||
// to authenticate indirectly (eg. to a database driver)
|
||||
// signingKey: {"kty":"oct","kid":"Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q","alg":"HS512","k":"K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"},
|
||||
|
||||
// If you chose something other than the default algorithm for the signingKey (HS512)
|
||||
// you also need to configure the algorithm
|
||||
// verificationOptions: {
|
||||
// algorithms: ['HS256']
|
||||
// },
|
||||
|
||||
// Set to true to use encryption. Defaults to false (signing only).
|
||||
// encryption: true,
|
||||
|
||||
// encryptionKey: "",
|
||||
// decryptionKey = encryptionKey,
|
||||
// decryptionOptions = {
|
||||
// algorithms: ['A256GCM']
|
||||
// },
|
||||
|
||||
// You can define your own encode/decode functions for signing and encryption
|
||||
// if you want to override the default behaviour.
|
||||
// async encode({ secret, token, maxAge }) {},
|
||||
|
||||
@@ -141,7 +141,7 @@ You can look at the existing built-in providers for inspiration.
|
||||
| profile | An callback returning an object with the user's info | `object` | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||
| headers | Any headers that should be sent to the OAuth provider | `object` | No |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) | `pkce`, `state`, `none` | No |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) |`[pkce]`,`[state]`,`[pkce,state]`| No |
|
||||
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
|
||||
|
||||
## Sign in with Email
|
||||
|
||||
@@ -76,6 +76,7 @@ In _most cases_ it does not make sense to specify a database in NextAuth.js opti
|
||||
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4.2).
|
||||
The `code_verifier` is saved in a cookie called (by default) `__Secure-next-auth.pkce.code_verifier` which expires after 15 minutes.
|
||||
Check if `cookies.pkceCodeVerifier` is configured correctly. The default `code_challenge_method` is `"S256"`. This is currently not configurable to `"plain"`, as it is not recommended, and in most cases it is only supported for backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
### Session Handling
|
||||
|
||||
@@ -54,10 +54,15 @@ NextAuth.js does not currently support automatically signing into sites on diffe
|
||||
|
||||
### Can I use NextAuth.js with React Native?
|
||||
|
||||
NextAuth.js is designed as a secure, confidental client and implements a server side authentication flow.
|
||||
NextAuth.js is designed as a secure, confidential client and implements a server side authentication flow.
|
||||
|
||||
It is not intended to be used in native applications on desktop or mobile applications, which typically implement public clients (e.g. with client / secrets embedded in the application).
|
||||
|
||||
|
||||
### Is NextAuth.js supporting TypeScript?
|
||||
|
||||
Yes! Check out the [TypeScript docs](/getting-started/typescript)
|
||||
|
||||
---
|
||||
|
||||
## Databases
|
||||
@@ -70,7 +75,7 @@ It also provides an Adapter API which allows you to connect it to any database.
|
||||
|
||||
### What does NextAuth.js use databases for?
|
||||
|
||||
Databases in NextAuth.js are used for persisting users, oauth accounts, email sign in tokens and sessions.
|
||||
Databases in NextAuth.js are used for persisting users, OAuth accounts, email sign in tokens and sessions.
|
||||
|
||||
Specifying a database is optional if you don't need to persist user data or support email sign in. If you don't specify a database then JSON Web Tokens will be enabled for session storage and used to store session data.
|
||||
|
||||
@@ -126,7 +131,7 @@ When an email address is associated with an OAuth account it does not necessaril
|
||||
|
||||
With automatic account linking on sign in, this can be exploited by bad actors to hijack accounts by creating an OAuth account associated with the email address of another user.
|
||||
|
||||
For this reason it is not secure to automatically link accounts between abitrary providers on sign in, which is why this feature is generally not provided by authentication service and is not provided by NextAuth.js.
|
||||
For this reason it is not secure to automatically link accounts between arbitrary providers on sign in, which is why this feature is generally not provided by authentication service and is not provided by NextAuth.js.
|
||||
|
||||
Automatic account linking is seen on some sites, sometimes insecurely. It can be technically possible to do automatic account linking securely if you trust all the providers involved to ensure they have securely verified the email address associated with the account, but requires placing trust (and transferring the risk) to those providers to handle the process securely.
|
||||
|
||||
@@ -169,7 +174,7 @@ NextAuth.js supports both database session tokens and JWT session tokens.
|
||||
* If a database is specified, database session tokens will be used by default.
|
||||
* If no database is specified, JWT session tokens will be used by default.
|
||||
|
||||
You can also choose to use JSON Web Tokens as session tokens with using a database, by explictly setting the `session: { jwt: true }` option.
|
||||
You can also choose to use JSON Web Tokens as session tokens with using a database, by explicitly setting the `session: { jwt: true }` option.
|
||||
|
||||
### What are the advantages of JSON Web Tokens?
|
||||
|
||||
@@ -201,13 +206,13 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
|
||||
|
||||
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
|
||||
|
||||
* If you do not explictly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
|
||||
* If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
|
||||
|
||||
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.
|
||||
|
||||
### Are JSON Web Tokens secure?
|
||||
|
||||
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space avalible to store data (total cookie size for a domain is limited to 4KB).
|
||||
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space available to store data (total cookie size for a domain is limited to 4KB).
|
||||
|
||||
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
|
||||
|
||||
@@ -217,7 +222,7 @@ You can specify other valid algorithms - [as specified in RFC 7518](https://tool
|
||||
|
||||
NextAuth.js will generate keys for you, but this will generate a warning at start up.
|
||||
|
||||
Using explict public/private keys for signing is strongly recommended.
|
||||
Using explicit public/private keys for signing is strongly recommended.
|
||||
|
||||
### What signing and encryption standards does NextAuth.js support?
|
||||
|
||||
|
||||
@@ -3,18 +3,19 @@ id: typescript
|
||||
title: TypeScript
|
||||
---
|
||||
|
||||
NextAuth.js comes with its own types, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions of what certain objects are, and sometimes also links to documentation, and examples.
|
||||
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about how certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
|
||||
|
||||
:::note
|
||||
:::warning
|
||||
The types at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) under the name of `@types/next-auth` are now deprecated, and not maintained anymore.
|
||||
:::
|
||||
|
||||
***
|
||||
## Module Augmentaion
|
||||
## Module Augmentation
|
||||
|
||||
`next-auth` comes with certain types/interfaces, that are shared across submodules. Good examples are `Session` and `JWT`. Ideally, you should only need to create these types at a single place, and TS should pick them up in every location where they are referenced. Luckily, this is exactly what Module Agumentation can do for us. Define your shared interfaces in a single location, and get type-safety across your application, when you use `next-auth` (or one of its submodules).
|
||||
`next-auth` comes with certain types/interfaces, that are shared across submodules. Good examples are `Session` and `JWT`. Ideally, you should only need to create these types at a single place, and TS should pick them up in every location where they are referenced. Luckily, this is exactly what Module Augmentation can do for us. Define your shared interfaces in a single location, and get type-safety across your application, when you use `next-auth` (or one of its submodules).
|
||||
|
||||
1. Let's look at `Session`:
|
||||
### Main module
|
||||
Let's look at `Session`:
|
||||
|
||||
```ts title="pages/api/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
@@ -47,6 +48,9 @@ To extend/augment this type, create a `types/next-auth.d.ts` file in your projec
|
||||
import NextAuth from "next-auth"
|
||||
|
||||
declare module "next-auth" {
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
|
||||
*/
|
||||
interface Session {
|
||||
user: {
|
||||
/** The user's postal address. */
|
||||
@@ -55,23 +59,47 @@ declare module "next-auth" {
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Popular interfaces to augment
|
||||
|
||||
Although you can augment almost anything, here are some of the more common interfaces that you might want to override in the `next-auth` module:
|
||||
```ts
|
||||
/**
|
||||
* The shape of the user object returned in the OAuth providers' `profile` callback,
|
||||
* or the second parameter of the `session` callback, when using a database.
|
||||
*/
|
||||
interface User {}
|
||||
/**
|
||||
* Usually contains information about the provider being used
|
||||
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
|
||||
*/
|
||||
interface Account {}
|
||||
/** The OAuth profile returned from your provider */
|
||||
interface Profile {}
|
||||
```
|
||||
|
||||
Make sure that the `types` folder is added to [`typeRoots`](https://www.typescriptlang.org/tsconfig/#typeRoots) in your project's `tsconfig.json` file.
|
||||
|
||||
2. Check out `JWT` also:
|
||||
### Submodules
|
||||
The `JWT` interface can be found in the `next-auth/jwt` submodule:
|
||||
|
||||
```ts title="types/next-auth.d.ts"
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
|
||||
interface JWT {
|
||||
/** OpenID ID Token */
|
||||
idToken?: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that this time we declared `JWT` inside `next-auth/jwt`, as this is its default location.
|
||||
|
||||
### Useful links
|
||||
1. [TypeScript documentation: Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
|
||||
2. [Digital Ocean: Module Augmentation in TypeScript](https://www.digitalocean.com/community/tutorials/typescript-module-augmentation)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions of any kind are always welcome, especially for TypeScript. Please keep in mind that we are a small team working on this project in our free time. We will try our best to give support, but if you think you have a solution for a problem, please open a PR!
|
||||
|
||||
:::note
|
||||
When contributing to TypeScript, if the actual JavaScript user API does not change in a breaking manner, we reserve the right to push any TypeScript change in a minor release. This is to ensure that we can keep us on a faster release cycle.
|
||||
:::
|
||||
@@ -11,6 +11,10 @@ https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-co
|
||||
|
||||
https://www.linkedin.com/developers/apps/
|
||||
|
||||
From the Auth tab get the client ID and client secret. On the same tab, add redirect URLs such as http://localhost:3000/api/auth/callback/linkedin so LinkedIn can correctly redirect back to your application. Finally, head over to the Products tab and enable the "Sign In with LinkedIn" product. The LinkedIn team will review and approve your request before you can test it out.
|
||||
|
||||

|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
@@ -185,7 +185,7 @@ Once you have saved your schema, use the Prisma CLI to generate the Prisma Clien
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
To configure you database to use the new schema (i.e. create tables and columns) use the `primsa migrate` command:
|
||||
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
|
||||
|
||||
```
|
||||
npx prisma migrate dev --preview-feature
|
||||
|
||||
@@ -11,7 +11,7 @@ _New submissions and edits are welcome!_
|
||||
|
||||
### [NextJS Authentication Crash Course with NextAuth.js](https://youtu.be/o_wZIVmWteQ)
|
||||
|
||||
This tutorial dives in to the ins and outs of NextAuth including email, Github, Twitter and integrating with Auth0 in under hour.
|
||||
This tutorial dives in to the ins and outs of NextAuth including email, GitHub, Twitter and integrating with Auth0 in under hour.
|
||||
|
||||
### [Create your own NextAuth.js Login Pages](https://youtu.be/kB6YNYZ63fw)
|
||||
|
||||
@@ -77,7 +77,7 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
|
||||
|
||||
### [Adding Authentication to an existing Next.js Application in no time!](https://dev.to/ndom91/adding-authentication-to-an-existing-serverless-next-js-app-in-no-time-with-nextauth-js-192h)
|
||||
|
||||
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and api routes behind that authentication, etc.
|
||||
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and API routes behind that authentication, etc.
|
||||
|
||||
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user