Auto-commit 2026-04-29 16:31

This commit is contained in:
2026-04-29 16:31:27 -04:00
parent e8687bb6b2
commit 0495ee5bd2
19691 changed files with 3272886 additions and 138 deletions

127
node_modules/next-auth/src/core/errors.ts generated vendored Normal file
View File

@@ -0,0 +1,127 @@
import type { EventCallbacks, InternalOptions, LoggerInstance } from ".."
/**
* Same as the default `Error`, but it is JSON serializable.
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {
code: string
constructor(error: Error | string) {
// Support passing error or string
super((error as Error)?.message ?? error)
this.name = "UnknownError"
this.code = (error as any).code
if (error instanceof Error) {
this.stack = error.stack
}
}
toJSON() {
return {
name: this.name,
message: this.message,
stack: this.stack,
}
}
}
export class OAuthCallbackError extends UnknownError {
name = "OAuthCallbackError"
}
/**
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class MissingAPIRoute extends UnknownError {
name = "MissingAPIRouteError"
code = "MISSING_NEXTAUTH_API_ROUTE_ERROR"
}
export class MissingSecret extends UnknownError {
name = "MissingSecretError"
code = "NO_SECRET"
}
export class MissingAuthorize extends UnknownError {
name = "MissingAuthorizeError"
code = "CALLBACK_CREDENTIALS_HANDLER_ERROR"
}
export class MissingAdapter extends UnknownError {
name = "MissingAdapterError"
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
}
export class InvalidCallbackUrl extends UnknownError {
name = "InvalidCallbackUrl"
code = "INVALID_CALLBACK_URL_ERROR"
}
type Method = (...args: any[]) => Promise<any>
export function upperSnake(s: string) {
return s.replace(/([A-Z])/g, "_$1").toUpperCase()
}
export function capitalize(s: string) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
/**
* Wraps an object of methods and adds error handling.
*/
export function eventsErrorHandler(
methods: Partial<EventCallbacks>,
logger: LoggerInstance
): Partial<EventCallbacks> {
return Object.keys(methods).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
const method: Method = methods[name as keyof Method]
return await method(...args)
} catch (e) {
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e as Error)
}
}
return acc
}, {})
}
/** Handles adapter induced errors. */
export function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
logger: LoggerInstance
): InternalOptions["adapter"] | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
logger.debug(`adapter_${name}`, { args })
const method: Method = adapter[name as keyof Method]
return await method(...args)
} catch (error) {
logger.error(`adapter_error_${name}`, error as Error)
const e = new UnknownError(error as Error)
e.name = `${capitalize(name)}Error`
throw e
}
}
return acc
}, {})
}

336
node_modules/next-auth/src/core/index.ts generated vendored Normal file
View File

@@ -0,0 +1,336 @@
import logger, { setLogger } from "../utils/logger"
import { detectOrigin } from "../utils/detect-origin"
import * as routes from "./routes"
import renderPage from "./pages"
import { init } from "./init"
import { assertConfig } from "./lib/assert"
import { SessionStore } from "./lib/cookie"
import type { AuthAction, AuthOptions } from "./types"
import type { Cookie } from "./lib/cookie"
import type { ErrorType } from "./pages/error"
import { parse as parseCookie } from "cookie"
export interface RequestInternal {
/** @default "http://localhost:3000" */
origin?: string
method?: string
cookies?: Partial<Record<string, string>>
headers?: Record<string, any>
query?: Record<string, any>
body?: Record<string, any>
action: AuthAction
providerId?: string
error?: string
}
export interface NextAuthHeader {
key: string
value: string
}
export interface ResponseInternal<
Body extends string | Record<string, any> | any[] = any
> {
status?: number
headers?: NextAuthHeader[]
body?: Body
redirect?: string
cookies?: Cookie[]
}
export interface NextAuthHandlerParams {
req: Request | RequestInternal
options: AuthOptions
}
async function getBody(req: Request): Promise<Record<string, any> | undefined> {
try {
return await req.json()
} catch {}
}
// TODO:
async function toInternalRequest(
req: RequestInternal | Request
): Promise<RequestInternal> {
if (req instanceof Request) {
const url = new URL(req.url)
// TODO: handle custom paths?
const nextauth = url.pathname.split("/").slice(3)
const headers = Object.fromEntries(req.headers)
const query: Record<string, any> = Object.fromEntries(url.searchParams)
query.nextauth = nextauth
return {
action: nextauth[0] as AuthAction,
method: req.method,
headers,
body: await getBody(req),
cookies: parseCookie(req.headers.get("cookie") ?? ""),
providerId: nextauth[1],
error: url.searchParams.get("error") ?? nextauth[1],
origin: detectOrigin(
headers["x-forwarded-host"] ?? headers.host,
headers["x-forwarded-proto"]
),
query,
}
}
const { headers } = req
const host = headers?.["x-forwarded-host"] ?? headers?.host
req.origin = detectOrigin(host, headers?.["x-forwarded-proto"])
return req
}
export async function AuthHandler<
Body extends string | Record<string, any> | any[]
>(params: NextAuthHandlerParams): Promise<ResponseInternal<Body>> {
const { options: authOptions, req: incomingRequest } = params
const req = await toInternalRequest(incomingRequest)
setLogger(authOptions.logger, authOptions.debug)
const assertionResult = assertConfig({ options: authOptions, req })
if (Array.isArray(assertionResult)) {
assertionResult.forEach(logger.warn)
} else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config
logger.error(assertionResult.code, assertionResult)
const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: [{ key: "Content-Type", value: "application/json" }],
body: { message } as any,
}
}
const { pages, theme } = authOptions
const authOnErrorPage =
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {
logger.error(
"AUTH_ON_ERROR_PAGE_ERROR",
new Error(
`The error page ${pages?.error} should not require authentication`
)
)
}
const render = renderPage({ theme })
return render.error({ error: "configuration" })
}
return {
redirect: `${pages.error}?error=Configuration`,
}
}
const { action, providerId, error, method = "GET" } = req
const { options, cookies } = await init({
authOptions,
action,
providerId,
origin: req.origin,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
isPost: method === "POST",
})
const sessionStore = new SessionStore(
options.cookies.sessionToken,
req,
options.logger
)
if (method === "GET") {
const render = renderPage({ ...options, query: req.query, cookies })
const { pages } = options
switch (action) {
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session({ options, sessionStore })
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
case "csrf":
return {
headers: [
{ key: "Content-Type", value: "application/json" },
{
key: "Cache-Control",
value: "private, no-cache, no-store",
},
{
key: "Pragma",
value: "no-cache",
},
{
key: "Expires",
value: "0",
},
],
body: { csrfToken: options.csrfToken } as any,
cookies,
}
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(options.callbackUrl)}`
if (error)
signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
return { redirect: signinUrl, cookies }
}
return render.signin()
case "signout":
if (pages.signOut) return { redirect: pages.signOut, cookies }
return render.signout()
case "callback":
if (options.provider) {
const callback = await routes.callback({
body: req.body,
query: req.query,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "verify-request":
if (pages.verifyRequest) {
return { redirect: pages.verifyRequest, cookies }
}
return render.verifyRequest()
case "error":
// These error messages are displayed in line on the sign in page
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}
return render.error({ error: error as ErrorType })
default:
}
} else if (method === "POST") {
switch (action) {
case "signin":
// Verified CSRF Token required for all sign-in routes
if (options.csrfTokenVerified && options.provider) {
const signin = await routes.signin({
query: req.query,
body: req.body,
options,
})
if (signin.cookies) cookies.push(...signin.cookies)
return { ...signin, cookies }
}
return { redirect: `${options.url}/signin?csrf=true`, cookies }
case "signout":
// Verified CSRF Token required for signout
if (options.csrfTokenVerified) {
const signout = await routes.signout({ options, sessionStore })
if (signout.cookies) cookies.push(...signout.cookies)
return { ...signout, cookies }
}
return { redirect: `${options.url}/signout?csrf=true`, cookies }
case "callback":
if (options.provider) {
// Verified CSRF Token required for credentials providers only
if (
options.provider.type === "credentials" &&
!options.csrfTokenVerified
) {
return { redirect: `${options.url}/signin?csrf=true`, cookies }
}
const callback = await routes.callback({
body: req.body,
query: req.query,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "_log": {
if (authOptions.logger) {
try {
const { code, level, ...metadata } = req.body ?? {}
logger[level](code, metadata)
} catch (error) {
// If logging itself failed...
logger.error("LOGGER_ERROR", error as Error)
}
}
return {}
}
case "session": {
// Verified CSRF Token required for session updates
if (options.csrfTokenVerified) {
const session = await routes.session({
options,
sessionStore,
newSession: req.body?.data,
isUpdate: true,
})
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
// If CSRF token is invalid, return a 400 status code
// we should not redirect to a page as this is an API route
return { status: 400, body: {} as any, cookies }
}
default:
}
}
return {
status: 400,
body: `Error: This action with HTTP ${method} is not supported by NextAuth.js` as any,
}
}

155
node_modules/next-auth/src/core/init.ts generated vendored Normal file
View File

@@ -0,0 +1,155 @@
import { randomBytes, randomUUID } from "crypto"
import { AuthOptions } from ".."
import logger from "../utils/logger"
import { adapterErrorHandler, eventsErrorHandler } from "./errors"
import parseProviders from "./lib/providers"
import { createSecret } from "./lib/utils"
import * as cookie from "./lib/cookie"
import * as jwt from "../jwt"
import { defaultCallbacks } from "./lib/default-callbacks"
import { createCSRFToken } from "./lib/csrf-token"
import { createCallbackUrl } from "./lib/callback-url"
import { RequestInternal } from "."
import type { InternalOptions } from "./types"
import parseUrl from "../utils/parse-url"
interface InitParams {
origin?: string
authOptions: AuthOptions
providerId?: string
action: InternalOptions["action"]
/** Callback URL value extracted from the incoming request. */
callbackUrl?: string
/** CSRF token value extracted from the incoming request. From body if POST, from query if GET */
csrfToken?: string
/** Is the incoming request a POST request? */
isPost: boolean
cookies: RequestInternal["cookies"]
}
/** Initialize all internal options and cookies. */
export async function init({
authOptions,
providerId,
action,
origin,
cookies: reqCookies,
callbackUrl: reqCallbackUrl,
csrfToken: reqCsrfToken,
isPost,
}: InitParams): Promise<{
options: InternalOptions
cookies: cookie.Cookie[]
}> {
const url = parseUrl(origin)
const secret = createSecret({ authOptions, url })
const { providers, provider } = parseProviders({
providers: authOptions.providers,
url,
providerId,
})
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
// User provided options are overriden by other options,
// except for the options with special handling above
const options: InternalOptions = {
debug: false,
pages: {},
theme: {
colorScheme: "auto",
logo: "",
brandColor: "",
buttonText: "",
},
// Custom options override defaults
...authOptions,
// These computed settings can have values in authOptions but we override them
// and are request-specific.
url,
action,
// @ts-expect-errors
provider,
cookies: {
...cookie.defaultCookies(
authOptions.useSecureCookies ?? url.base.startsWith("https://")
),
// Allow user cookie options to override any cookie settings above
...authOptions.cookies,
},
secret,
providers,
// Session options
session: {
// If no adapter specified, force use of JSON Web Tokens (stateless)
strategy: authOptions.adapter ? "database" : "jwt",
maxAge,
updateAge: 24 * 60 * 60,
generateSessionToken: () => {
// Use `randomUUID` if available. (Node 15.6+)
return randomUUID?.() ?? randomBytes(32).toString("hex")
},
...authOptions.session,
},
// JWT options
jwt: {
secret, // Use application secret if no keys specified
maxAge, // same as session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...authOptions.jwt,
},
// Event messages
events: eventsErrorHandler(authOptions.events ?? {}, logger),
adapter: adapterErrorHandler(authOptions.adapter, logger),
// Callback functions
callbacks: { ...defaultCallbacks, ...authOptions.callbacks },
logger,
callbackUrl: url.origin,
}
// Init cookies
const cookies: cookie.Cookie[] = []
const {
csrfToken,
cookie: csrfCookie,
csrfTokenVerified,
} = createCSRFToken({
options,
cookieValue: reqCookies?.[options.cookies.csrfToken.name],
isPost,
bodyValue: reqCsrfToken,
})
options.csrfToken = csrfToken
options.csrfTokenVerified = csrfTokenVerified
if (csrfCookie) {
cookies.push({
name: options.cookies.csrfToken.name,
value: csrfCookie,
options: options.cookies.csrfToken.options,
})
}
const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({
options,
cookieValue: reqCookies?.[options.cookies.callbackUrl.name],
paramValue: reqCallbackUrl,
})
options.callbackUrl = callbackUrl
if (callbackUrlCookie) {
cookies.push({
name: options.cookies.callbackUrl.name,
value: callbackUrlCookie,
options: options.cookies.callbackUrl.options,
})
}
return { options, cookies }
}

149
node_modules/next-auth/src/core/lib/assert.ts generated vendored Normal file
View File

@@ -0,0 +1,149 @@
import {
MissingAdapter,
MissingAPIRoute,
MissingAuthorize,
MissingSecret,
UnsupportedStrategy,
InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors"
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"
import type { RequestInternal } from ".."
import type { WarningCode } from "../../utils/logger"
import type { AuthOptions } from "../types"
type ConfigError =
| MissingAPIRoute
| MissingSecret
| UnsupportedStrategy
| MissingAuthorize
| MissingAdapter
let warned = false
function isValidHttpUrl(url: string, baseUrl: string) {
try {
return /^https?:/.test(
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
)
} catch {
return false
}
}
/**
* Verify that the user configured `next-auth` correctly.
* Good place to mention deprecations as well.
*
* REVIEW: Make some of these and corresponding docs less Next.js specific?
*/
export function assertConfig(params: {
options: AuthOptions
req: RequestInternal
}): ConfigError | WarningCode[] {
const { options, req } = params
const warnings: WarningCode[] = []
if (!warned) {
if (!req.origin) warnings.push("NEXTAUTH_URL")
// TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV`
if (!options.secret && process.env.NODE_ENV !== "production")
warnings.push("NO_SECRET")
if (options.debug) warnings.push("DEBUG_ENABLED")
}
if (!options.secret && process.env.NODE_ENV === "production") {
return new MissingSecret("Please define a `secret` in production.")
}
// req.query isn't defined when asserting `getServerSession` for example
if (!req.query?.nextauth && !req.action) {
return new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
)
}
const callbackUrlParam = req.query?.callbackUrl as string | undefined
const url = parseUrl(req.origin)
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
return new InvalidCallbackUrl(
`Invalid callback URL. Received: ${callbackUrlParam}`
)
}
const { callbackUrl: defaultCallbackUrl } = defaultCookies(
options.useSecureCookies ?? url.base.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) {
return new InvalidCallbackUrl(
`Invalid callback URL. Received: ${callbackUrlCookie}`
)
}
let hasCredentials, hasEmail
let hasTwitterOAuth2
for (const provider of options.providers) {
if (provider.type === "credentials") hasCredentials = true
else if (provider.type === "email") hasEmail = true
else if (provider.id === "twitter" && provider.version === "2.0")
hasTwitterOAuth2 = true
}
if (hasCredentials) {
const dbStrategy = options.session?.strategy === "database"
const onlyCredentials = !options.providers.some(
(p) => p.type !== "credentials"
)
if (dbStrategy && onlyCredentials) {
return new UnsupportedStrategy(
"Signin in with credentials only supported if JWT strategy is enabled"
)
}
const credentialsNoAuthorize = options.providers.some(
(p) => p.type === "credentials" && !p.authorize
)
if (credentialsNoAuthorize) {
return new MissingAuthorize(
"Must define an authorize() handler to use credentials authentication provider"
)
}
}
if (hasEmail) {
const { adapter } = options
if (!adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
const missingMethods = [
"createVerificationToken",
"useVerificationToken",
"getUserByEmail",
].filter((method) => !adapter[method])
if (missingMethods.length) {
return new MissingAdapterMethods(
`Required adapter methods were missing: ${missingMethods.join(", ")}`
)
}
}
if (!warned) {
if (hasTwitterOAuth2) warnings.push("TWITTER_OAUTH_2_BETA")
warned = true
}
return warnings
}

233
node_modules/next-auth/src/core/lib/callback-handler.ts generated vendored Normal file
View File

@@ -0,0 +1,233 @@
import { AccountNotLinkedError } from "../errors"
import { fromDate } from "./utils"
import type { InternalOptions } from "../types"
import type { AdapterSession, AdapterUser } from "../../adapters"
import type { JWT } from "../../jwt"
import type { Account, User } from "../.."
import type { SessionToken } from "./cookie"
import { OAuthConfig } from "src/providers"
/**
* This function handles the complex flow of signing users in, and either creating,
* linking (or not linking) accounts depending on if the user is currently logged
* in, if they have account already and the authentication mechanism they are using.
*
* It prevents insecure behaviour, such as linking OAuth accounts unless a user is
* signed in and authenticated with an existing valid account.
*
* All verification (e.g. OAuth flows or email address verificaiton flows) are
* done prior to this handler being called to avoid additonal complexity in this
* handler.
*/
export default async function callbackHandler(params: {
sessionToken?: SessionToken
profile: User | AdapterUser | { email: string }
account: Account | null
options: InternalOptions
}) {
const { sessionToken, profile: _profile, account, options } = params
// Input validation
if (!account?.providerAccountId || !account.type)
throw new Error("Missing or invalid provider account")
if (!["email", "oauth"].includes(account.type))
throw new Error("Provider not supported")
const {
adapter,
jwt,
events,
session: { strategy: sessionStrategy, generateSessionToken },
} = options
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return { user: _profile as User, account }
}
const profile = _profile as AdapterUser
const {
createUser,
updateUser,
getUser,
getUserByAccount,
getUserByEmail,
linkAccount,
createSession,
getSessionAndUser,
deleteSession,
} = adapter
let session: AdapterSession | JWT | null = null
let user: AdapterUser | null = null
let isNewUser = false
const useJwtSession = sessionStrategy === "jwt"
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ ...jwt, token: sessionToken })
if (session && "sub" in session && session.sub) {
user = await getUser(session.sub)
}
} catch {
// If session can't be verified, treat as no session
}
} else {
const userAndSession = await getSessionAndUser(sessionToken)
if (userAndSession) {
session = userAndSession.session
user = userAndSession.user
}
}
}
if (account.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = await getUserByEmail(profile.email)
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
if (user?.id !== userByEmail.id && !useJwtSession && sessionToken) {
// Delete existing session if they are currently signed in as another user.
// This will switch user accounts for the session in cases where the user was
// already logged in with a different account.
await deleteSession(sessionToken)
}
// Update emailVerified property on the user object
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
await events.updateUser?.({ user })
} else {
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
// Create user account if there isn't one for the email address already
// @ts-expect-error see adapters.ts' FutureAdapter["createUser"]
user = await createUser(newUser)
await events.createUser?.({ user })
isNewUser = true
}
// Create new session
session = useJwtSession
? {}
: await createSession({
sessionToken: await generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
return { session, user, isNewUser }
} else if (account.type === "oauth") {
// If signing in with OAuth account, check to see if the account exists already
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) {
if (user) {
// If the user is already signed in with this account, we don't need to do anything
if (userByAccount.id === user.id) {
return { session, user, isNewUser }
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new AccountNotLinkedError(
"The account is already associated with another user"
)
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession
? {}
: await createSession({
sessionToken: await generateSessionToken(),
userId: userByAccount.id,
expires: fromDate(options.session.maxAge),
})
return { session, user: userByAccount, isNewUser }
} else {
if (user) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
// @ts-expect-error see adapters.ts' FutureAdapter["linkAccount"]
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
// As they are already signed in, we don't need to do anything after linking them
return { session, user, isNewUser }
}
// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
const provider = options.provider as OAuthConfig<any>
if (provider?.allowDangerousEmailAccountLinking) {
// If you trust the oauth provider to correctly verify email addresses, you can opt-in to
// account linking even when the user is not signed-in.
user = userByEmail
} else {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts.
throw new AccountNotLinkedError(
"Another account already exists with the same e-mail address"
)
}
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
// @ts-expect-error see adapters.ts' FutureAdapter["createUser"]
user = await createUser(newUser)
}
await events.createUser?.({ user })
// @ts-expect-error see adapters.ts' FutureAdapter["linkAccount"]
await linkAccount({ ...account, userId: user.id })
await events.linkAccount?.({ user, account, profile })
session = useJwtSession
? {}
: await createSession({
sessionToken: await generateSessionToken(),
userId: user.id,
expires: fromDate(options.session.maxAge),
})
return { session, user, isNewUser: true }
}
}
throw new Error("Unsupported account type")
}

42
node_modules/next-auth/src/core/lib/callback-url.ts generated vendored Normal file
View File

@@ -0,0 +1,42 @@
import type { InternalOptions } from "../types"
interface CreateCallbackUrlParams {
options: InternalOptions
/** Try reading value from request body (POST) then from query param (GET) */
paramValue?: string
cookieValue?: string
}
/**
* Get callback URL based on query param / cookie + validation,
* and add it to `req.options.callbackUrl`.
*/
export async function createCallbackUrl({
options,
paramValue,
cookieValue,
}: CreateCallbackUrlParams) {
const { url, callbacks } = options
let callbackUrl = url.origin
if (paramValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect({
url: paramValue,
baseUrl: url.origin,
})
} else if (cookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect({
url: cookieValue,
baseUrl: url.origin,
})
}
return {
callbackUrl,
// Save callback URL in a cookie so that it can be used for subsequent requests in signin/signout/callback flow
callbackUrlCookie: callbackUrl !== cookieValue ? callbackUrl : undefined,
}
}

250
node_modules/next-auth/src/core/lib/cookie.ts generated vendored Normal file
View File

@@ -0,0 +1,250 @@
import type { CookiesOptions } from "../.."
import type { CookieOption, LoggerInstance, SessionStrategy } from "../types"
import type { NextRequest } from "next/server"
import type { NextApiRequest } from "next"
// Uncomment to recalculate the estimated size
// of an empty session cookie
// import { serialize } from "cookie"
// console.log(
// "Cookie estimated to be ",
// serialize(`__Secure.next-auth.session-token.0`, "", {
// expires: new Date(),
// httpOnly: true,
// maxAge: Number.MAX_SAFE_INTEGER,
// path: "/",
// sameSite: "strict",
// secure: true,
// domain: "example.com",
// }).length,
// " bytes"
// )
const ALLOWED_COOKIE_SIZE = 4096
// Based on commented out section above
const ESTIMATED_EMPTY_COOKIE_SIZE = 163
const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE
// REVIEW: Is there any way to defer two types of strings?
/** Stringified form of `JWT`. Extract the content with `jwt.decode` */
export type JWTString = string
export type SetCookieOptions = Partial<CookieOption["options"]> & {
expires?: Date | string
encode?: (val: unknown) => string
}
/**
* If `options.session.strategy` is set to `jwt`, this is a stringified `JWT`.
* In case of `strategy: "database"`, this is the `sessionToken` of the session in the database.
*/
export type SessionToken<T extends SessionStrategy = "jwt"> = T extends "jwt"
? JWTString
: string
/**
* Use secure cookies if the site uses HTTPS
* This being conditional allows cookies to work non-HTTPS development URLs
* Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
* prefix, but enable them by default if the site URL is HTTPS; but not for
* non-HTTPS URLs like http://localhost which are used in development).
* For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
*
* @TODO Review cookie settings (names, options)
*/
export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
const cookiePrefix = useSecureCookies ? "__Secure-" : ""
return {
// default cookie options
sessionToken: {
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? "__Host-" : ""}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
pkceCodeVerifier: {
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 60 * 15, // 15 minutes in seconds
},
},
state: {
name: `${cookiePrefix}next-auth.state`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
maxAge: 60 * 15, // 15 minutes in seconds
},
},
nonce: {
name: `${cookiePrefix}next-auth.nonce`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
}
}
export interface Cookie extends CookieOption {
value: string
}
type Chunks = Record<string, string>
export class SessionStore {
#chunks: Chunks = {}
#option: CookieOption
#logger: LoggerInstance | Console
constructor(
option: CookieOption,
req: Partial<{
cookies: NextRequest["cookies"] | NextApiRequest["cookies"]
headers: NextRequest["headers"] | NextApiRequest["headers"]
}>,
logger: LoggerInstance | Console
) {
this.#logger = logger
this.#option = option
const { cookies } = req
const { name: cookieName } = option
if (typeof cookies?.getAll === "function") {
// Next.js ^v13.0.1 (Edge Env)
for (const { name, value } of cookies.getAll()) {
if (name.startsWith(cookieName)) {
this.#chunks[name] = value
}
}
} else if (cookies instanceof Map) {
for (const name of cookies.keys()) {
if (name.startsWith(cookieName)) this.#chunks[name] = cookies.get(name)
}
} else {
for (const name in cookies) {
if (name.startsWith(cookieName)) this.#chunks[name] = cookies[name]
}
}
}
/**
* The JWT Session or database Session ID
* constructed from the cookie chunks.
*/
get value() {
// Sort the chunks by their keys before joining
const sortedKeys = Object.keys(this.#chunks).sort((a, b) => {
const aSuffix = parseInt(a.split(".").pop() ?? "0")
const bSuffix = parseInt(b.split(".").pop() ?? "0")
return aSuffix - bSuffix
})
// Use the sorted keys to join the chunks in the correct order
return sortedKeys.map((key) => this.#chunks[key]).join("")
}
/** Given a cookie, return a list of cookies, chunked to fit the allowed cookie size. */
#chunk(cookie: Cookie): Cookie[] {
const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE)
if (chunkCount === 1) {
this.#chunks[cookie.name] = cookie.value
return [cookie]
}
const cookies: Cookie[] = []
for (let i = 0; i < chunkCount; i++) {
const name = `${cookie.name}.${i}`
const value = cookie.value.substr(i * CHUNK_SIZE, CHUNK_SIZE)
cookies.push({ ...cookie, name, value })
this.#chunks[name] = value
}
this.#logger.debug("CHUNKING_SESSION_COOKIE", {
message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
valueSize: cookie.value.length,
chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
})
return cookies
}
/** Returns cleaned cookie chunks. */
#clean(): Record<string, Cookie> {
const cleanedChunks: Record<string, Cookie> = {}
for (const name in this.#chunks) {
delete this.#chunks?.[name]
cleanedChunks[name] = {
name,
value: "",
options: { ...this.#option.options, maxAge: 0 },
}
}
return cleanedChunks
}
/**
* Given a cookie value, return new cookies, chunked, to fit the allowed cookie size.
* If the cookie has changed from chunked to unchunked or vice versa,
* it deletes the old cookies as well.
*/
chunk(value: string, options: Partial<Cookie["options"]>): Cookie[] {
// Assume all cookies should be cleaned by default
const cookies: Record<string, Cookie> = this.#clean()
// Calculate new chunks
const chunked = this.#chunk({
name: this.#option.name,
value,
options: { ...this.#option.options, ...options },
})
// Update stored chunks / cookies
for (const chunk of chunked) {
cookies[chunk.name] = chunk
}
return Object.values(cookies)
}
/** Returns a list of cookies that should be cleaned. */
clean(): Cookie[] {
return Object.values(this.#clean())
}
}

55
node_modules/next-auth/src/core/lib/csrf-token.ts generated vendored Normal file
View File

@@ -0,0 +1,55 @@
import { createHash, randomBytes } from "crypto"
import type { InternalOptions } from "../types"
interface CreateCSRFTokenParams {
options: InternalOptions
cookieValue?: string
isPost: boolean
bodyValue?: string
}
/**
* Ensure CSRF Token cookie is set for any subsequent requests.
* Used as part of the strategy for mitigation for CSRF tokens.
*
* Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
* where 'token' is the CSRF token and 'hash' is a hash made of the token and
* the secret, and the two values are joined by a pipe '|'. By storing the
* value and the hash of the value (with the secret used as a salt) we can
* verify the cookie was set by the server and not by a malicous attacker.
*
* 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
*/
export function createCSRFToken({
options,
cookieValue,
isPost,
bodyValue,
}: CreateCSRFTokenParams) {
if (cookieValue) {
const [csrfToken, csrfTokenHash] = cookieValue.split("|")
const expectedCsrfTokenHash = createHash("sha256")
.update(`${csrfToken}${options.secret}`)
.digest("hex")
if (csrfTokenHash === expectedCsrfTokenHash) {
// If hash matches then we trust the CSRF token value
// 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 = isPost && csrfToken === bodyValue
return { csrfTokenVerified, csrfToken }
}
}
// New CSRF token
const csrfToken = randomBytes(32).toString("hex")
const csrfTokenHash = createHash("sha256")
.update(`${csrfToken}${options.secret}`)
.digest("hex")
const cookie = `${csrfToken}|${csrfTokenHash}`
return { cookie, csrfToken }
}

View File

@@ -0,0 +1,18 @@
import { CallbacksOptions } from "../.."
export const defaultCallbacks: CallbacksOptions = {
signIn() {
return true
},
redirect({ url, baseUrl }) {
if (url.startsWith("/")) return `${baseUrl}${url}`
else if (new URL(url).origin === baseUrl) return url
return baseUrl
},
session({ session }) {
return session
},
jwt({ token }) {
return token
},
}

View File

@@ -0,0 +1,21 @@
import type { AdapterUser } from "../../../adapters"
import type { InternalOptions } from "../../types"
/**
* Query the database for a user by email address.
* If is an existing user return a user object (otherwise use placeholder).
*/
export default async function getAdapterUserFromEmail({
email,
adapter,
}: {
email: string
adapter: InternalOptions<"email">["adapter"]
}): Promise<AdapterUser> {
// @ts-expect-error -- adapter is checked to be defined in `init`
const { getUserByEmail } = adapter
const adapterUser = email ? await getUserByEmail(email) : null
if (adapterUser) return adapterUser
return { id: email, email, emailVerified: null }
}

51
node_modules/next-auth/src/core/lib/email/signin.ts generated vendored Normal file
View File

@@ -0,0 +1,51 @@
import { randomBytes } from "crypto"
import { hashToken } from "../utils"
import type { InternalOptions } from "../../types"
/**
* Starts an e-mail login flow, by generating a token,
* and sending it to the user's e-mail (with the help of a DB adapter)
*/
export default async function email(
identifier: string,
options: InternalOptions<"email">
): Promise<string> {
const { url, adapter, provider, callbackUrl, theme } = options
// Generate token
const token =
(await provider.generateVerificationToken?.()) ??
randomBytes(32).toString("hex")
const ONE_DAY_IN_SECONDS = 86400
const expires = new Date(
Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000
)
// Generate a link with email, unhashed token and callback url
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
const _url = `${url}/callback/${provider.id}?${params}`
await Promise.all([
// Send to user
provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
}),
// Save in database
// @ts-expect-error -- adapter is checked to be defined in `init`
adapter.createVerificationToken?.({
identifier,
token: hashToken(token, options),
expires,
}),
])
return `${url}/verify-request?${new URLSearchParams({
provider: provider.id,
type: provider.type,
})}`
}

View File

@@ -0,0 +1,63 @@
import { openidClient } from "./client"
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
import * as checks from "./checks"
import type { AuthorizationParameters } from "openid-client"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { Cookie } from "../cookie"
/**
*
* Generates an authorization/request token URL.
*
* [OAuth 2](https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/) | [OAuth 1](https://oauth.net/core/1.0a/#auth_step2)
*/
export default async function getAuthorizationUrl({
options,
query,
}: {
options: InternalOptions<"oauth">
query: RequestInternal["query"]
}) {
const { logger, provider } = options
let params: any = {}
if (typeof provider.authorization === "string") {
const parsedUrl = new URL(provider.authorization)
const parsedParams = Object.fromEntries(parsedUrl.searchParams)
params = { ...params, ...parsedParams }
} else {
params = { ...params, ...provider.authorization?.params }
}
params = { ...params, ...query }
// Handle OAuth v1.x
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${provider.authorization?.url}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
})}`
oAuth1TokenStore.set(tokens.oauth_token, tokens.oauth_token_secret)
logger.debug("GET_AUTHORIZATION_URL", { url, provider })
return { redirect: url }
}
const client = await openidClient(options)
const authorizationParams: AuthorizationParameters = params
const cookies: Cookie[] = []
await checks.state.create(options, cookies, authorizationParams)
await checks.pkce.create(options, cookies, authorizationParams)
await checks.nonce.create(options, cookies, authorizationParams)
const url = client.authorizationUrl(authorizationParams)
logger.debug("GET_AUTHORIZATION_URL", { url, cookies, provider })
return { redirect: url, cookies }
}

183
node_modules/next-auth/src/core/lib/oauth/callback.ts generated vendored Normal file
View File

@@ -0,0 +1,183 @@
import { TokenSet } from "openid-client"
import { openidClient } from "./client"
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
import * as _checks from "./checks"
import { OAuthCallbackError } from "../../errors"
import type { CallbackParamsType } from "openid-client"
import type { LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { Cookie } from "../cookie"
export default async function oAuthCallback(params: {
options: InternalOptions<"oauth">
query: RequestInternal["query"]
body: RequestInternal["body"]
method: Required<RequestInternal>["method"]
cookies: RequestInternal["cookies"]
}) {
const { options, query, body, method, cookies } = params
const { logger, provider } = options
const errorMessage = body?.error ?? query?.error
if (errorMessage) {
const error = new Error(errorMessage)
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", {
error,
error_description: query?.error_description,
providerId: provider.id,
})
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}
if (provider.version?.startsWith("1.")) {
try {
const client = await oAuth1Client(options)
// Handle OAuth v1.x
const { oauth_token, oauth_verifier } = query ?? {}
const tokens = (await (client as any).getOAuthAccessToken(
oauth_token,
oAuth1TokenStore.get(oauth_token),
oauth_verifier
)) as TokenSet
let profile: Profile = await (client as any).get(
provider.profileUrl,
tokens.oauth_token,
tokens.oauth_token_secret
)
if (typeof profile === "string") {
profile = JSON.parse(profile)
}
const newProfile = await getProfile({ profile, tokens, provider, logger })
return { ...newProfile, cookies: [] }
} catch (error) {
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
throw error
}
}
if (query?.oauth_token) oAuth1TokenStore.delete(query.oauth_token)
try {
const client = await openidClient(options)
let tokens: TokenSet
const checks: OAuthChecks = {}
const resCookies: Cookie[] = []
await _checks.state.use(cookies, resCookies, options, checks)
await _checks.pkce.use(cookies, resCookies, options, checks)
await _checks.nonce.use(cookies, resCookies, options, checks)
const params: CallbackParamsType = {
...client.callbackParams({
url: `http://n?${new URLSearchParams(query)}`,
// TODO: Ask to allow object to be passed upstream:
// https://github.com/panva/node-openid-client/blob/3ae206dfc78c02134aa87a07f693052c637cab84/types/index.d.ts#L439
// @ts-expect-error
body,
method,
}),
...provider.token?.params,
}
if (provider.token?.request) {
const response = await provider.token.request({
provider,
params,
checks,
client,
})
tokens = new TokenSet(response.tokens)
} else if (provider.idToken) {
tokens = await client.callback(provider.callbackUrl, params, checks)
} else {
tokens = await client.oauthCallback(provider.callbackUrl, params, checks)
}
// REVIEW: How can scope be returned as an array?
if (Array.isArray(tokens.scope)) {
tokens.scope = tokens.scope.join(" ")
}
let profile: Profile
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({
provider,
tokens,
client,
})
} else if (provider.idToken) {
profile = tokens.claims()
} else {
profile = await client.userinfo(tokens, {
params: provider.userinfo?.params,
})
}
const profileResult = await getProfile({
profile,
provider,
tokens,
logger,
})
return { ...profileResult, cookies: resCookies }
} catch (error) {
throw new OAuthCallbackError(error as Error)
}
}
export interface GetProfileParams {
profile: Profile
tokens: TokenSet
provider: OAuthConfig<any>
logger: LoggerInstance
}
/** Returns profile, raw profile and auth provider details */
async function getProfile({
profile: OAuthProfile,
tokens,
provider,
logger,
}: GetProfileParams) {
try {
logger.debug("PROFILE_DATA", { OAuthProfile })
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
if (!profile.id)
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
)
// Return profile, raw profile and auth provider details
return {
profile,
account: {
provider: provider.id,
type: provider.type,
providerAccountId: profile.id.toString(),
...tokens,
},
OAuthProfile,
}
} catch (error) {
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortuately, we can't tell which - at least not in a way that works for
// 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", {
error: error as Error,
OAuthProfile,
})
}
}

199
node_modules/next-auth/src/core/lib/oauth/checks.ts generated vendored Normal file
View File

@@ -0,0 +1,199 @@
import {
AuthorizationParameters,
generators,
OpenIDCallbackChecks,
} from "openid-client"
import * as jwt from "../../../jwt"
import type { RequestInternal } from "../.."
import type { OAuthChecks } from "../../../providers"
import type { CookiesOptions, InternalOptions } from "../../types"
import type { Cookie } from "../cookie"
/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, logger } = options
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
const { name } = cookies[type]
const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name,
value: await jwt.encode({
...options.jwt,
maxAge,
token: { value },
salt: name,
}),
options: { ...cookies[type].options, expires },
}
}
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const PKCE_CODE_CHALLENGE_METHOD = "S256"
export const pkce = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider?.checks?.includes("pkce")) return
const code_verifier = generators.codeVerifier()
const value = generators.codeChallenge(code_verifier)
resParams.code_challenge = value
resParams.code_challenge_method = PKCE_CODE_CHALLENGE_METHOD
const maxAge =
options.cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
cookies.push(
await signCookie("pkceCodeVerifier", code_verifier, maxAge, options)
)
},
/**
* Returns code_verifier if the provider is configured to use PKCE,
* and clears the container cookie afterwards.
* An error is thrown if the code_verifier is missing or invalid.
* @see https://www.rfc-editor.org/rfc/rfc7636
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OAuthChecks
): Promise<string | undefined> {
if (!options.provider?.checks?.includes("pkce")) return
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
if (!codeVerifier)
throw new TypeError("PKCE code_verifier cookie was missing.")
const { name } = options.cookies.pkceCodeVerifier
const value = (await jwt.decode({
...options.jwt,
token: codeVerifier,
salt: name,
})) as any
if (!value?.value)
throw new TypeError("PKCE code_verifier value could not be parsed.")
resCookies.push({
name,
value: "",
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
})
checks.code_verifier = value.value
},
}
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const state = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider.checks?.includes("state")) return
const value = generators.state()
resParams.state = value
const maxAge = options.cookies.state.options.maxAge ?? STATE_MAX_AGE
cookies.push(await signCookie("state", value, maxAge, options))
},
/**
* Returns state if the provider is configured to use state,
* and clears the container cookie afterwards.
* An error is thrown if the state is missing or invalid.
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OAuthChecks
) {
if (!options.provider.checks?.includes("state")) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new TypeError("State cookie was missing.")
const { name } = options.cookies.state
const value = (await jwt.decode({
...options.jwt,
token: state,
salt: name,
})) as any
if (!value?.value) throw new TypeError("State value could not be parsed.")
resCookies.push({
name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
checks.state = value.value
},
}
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const nonce = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider.checks?.includes("nonce")) return
const value = generators.nonce()
resParams.nonce = value
const maxAge = options.cookies.nonce.options.maxAge ?? NONCE_MAX_AGE
cookies.push(await signCookie("nonce", value, maxAge, options))
},
/**
* Returns nonce if the provider is configured to use nonce,
* and clears the container cookie afterwards.
* An error is thrown if the nonce is missing or invalid.
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OpenIDCallbackChecks
): Promise<string | undefined> {
if (!options.provider?.checks?.includes("nonce")) return
const nonce = cookies?.[options.cookies.nonce.name]
if (!nonce) throw new TypeError("Nonce cookie was missing.")
const { name } = options.cookies.nonce
const value = (await jwt.decode({
...options.jwt,
token: nonce,
salt: name,
})) as any
if (!value?.value) throw new TypeError("Nonce value could not be parsed.")
resCookies.push({
name,
value: "",
options: { ...options.cookies.nonce.options, maxAge: 0 },
})
checks.nonce = value.value
},
}

View File

@@ -0,0 +1,73 @@
// This is kept around for being backwards compatible with OAuth 1.0 providers.
// We have the intentions to provide only minor fixes for this in the future.
import { OAuth } from "oauth"
import type { InternalOptions } from "../../types"
/**
* Client supporting OAuth 1.x
*/
export function oAuth1Client(options: InternalOptions<"oauth">) {
const provider = options.provider
const oauth1Client = new OAuth(
provider.requestTokenUrl as string,
provider.accessTokenUrl as string,
provider.clientId as string,
provider.clientSecret as string,
provider.version ?? "1.0",
provider.callbackUrl,
provider.encoding ?? "HMAC-SHA1"
)
// Promisify get() for OAuth1
const originalGet = oauth1Client.get.bind(oauth1Client)
// @ts-expect-error
oauth1Client.get = async (...args) => {
return await new Promise((resolve, reject) => {
originalGet(...args, (error, result) => {
if (error) {
return reject(error)
}
resolve(result)
})
})
}
// Promisify getOAuth1AccessToken() for OAuth1
const originalGetOAuth1AccessToken =
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
oauth1Client.getOAuthAccessToken = async (...args: any[]) => {
return await new Promise((resolve, reject) => {
originalGetOAuth1AccessToken(
...args,
(error: any, oauth_token: any, oauth_token_secret: any) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret } as any)
}
)
})
}
const originalGetOAuthRequestToken =
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
oauth1Client.getOAuthRequestToken = async (params = {}) => {
return await new Promise((resolve, reject) => {
originalGetOAuthRequestToken(
params,
(error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret, params } as any)
}
)
})
}
return oauth1Client
}
export const oAuth1TokenStore = new Map()

48
node_modules/next-auth/src/core/lib/oauth/client.ts generated vendored Normal file
View File

@@ -0,0 +1,48 @@
import { Issuer, custom } from "openid-client"
import type { Client } from "openid-client"
import type { InternalOptions } from "../../types"
/**
* NOTE: We can add auto discovery of the provider's endpoint
* that requires only one endpoint to be specified by the user.
* Check out `Issuer.discover`
*
* Client supporting OAuth 2.x and OIDC
*/
export async function openidClient(
options: InternalOptions<"oauth">
): Promise<Client> {
const provider = options.provider
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
let issuer: Issuer
if (provider.wellKnown) {
issuer = await Issuer.discover(provider.wellKnown)
} else {
issuer = new Issuer({
issuer: provider.issuer as string,
authorization_endpoint: provider.authorization?.url,
token_endpoint: provider.token?.url,
userinfo_endpoint: provider.userinfo?.url,
jwks_uri: provider.jwks_endpoint,
})
}
const client = new issuer.Client(
{
client_id: provider.clientId as string,
client_secret: provider.clientSecret as string,
redirect_uris: [provider.callbackUrl],
...provider.client,
},
provider.jwks
)
// allow a 10 second skew
// See https://github.com/nextauthjs/next-auth/issues/3032
// and https://github.com/nextauthjs/next-auth/issues/3067
client[custom.clock_tolerance] = 10
return client
}

93
node_modules/next-auth/src/core/lib/providers.ts generated vendored Normal file
View File

@@ -0,0 +1,93 @@
import { merge } from "../../utils/merge"
import type { InternalProvider, OAuthConfigInternal } from "../types"
import type { OAuthConfig, Provider } from "../../providers"
import type { InternalUrl } from "../../utils/parse-url"
/**
* Adds `signinUrl` and `callbackUrl` to each provider
* and deep merge user-defined options.
*/
export default function parseProviders(params: {
providers: Provider[]
url: InternalUrl
providerId?: string
}): {
providers: InternalProvider[]
provider?: InternalProvider
} {
const { url, providerId } = params
const providers = params.providers.map<InternalProvider>(
({ options: userOptions, ...rest }) => {
if (rest.type === "oauth") {
const normalizedOptions = normalizeOAuthOptions(rest)
const normalizedUserOptions = normalizeOAuthOptions(userOptions, true)
const id = normalizedUserOptions?.id ?? rest.id
return merge(normalizedOptions, {
...normalizedUserOptions,
signinUrl: `${url}/signin/${id}`,
callbackUrl: `${url}/callback/${id}`,
})
}
const id = (userOptions?.id as string) ?? rest.id
return merge(rest, {
...userOptions,
signinUrl: `${url}/signin/${id}`,
callbackUrl: `${url}/callback/${id}`,
})
}
)
return {
providers,
provider: providers.find(({ id }) => id === providerId),
}
}
/**
* Transform OAuth options `authorization`, `token` and `profile` strings to `{ url: string; params: Record<string, string> }`
*/
function normalizeOAuthOptions(
oauthOptions?: Partial<OAuthConfig<any>> | Record<string, unknown>,
isUserOptions = false
) {
if (!oauthOptions) return
const normalized = Object.entries(oauthOptions).reduce<
OAuthConfigInternal<Record<string, unknown>>
>(
(acc, [key, value]) => {
if (
["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string"
) {
const url = new URL(value)
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key] = value
}
return acc
},
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
{} as any
)
if (!isUserOptions && !normalized.version?.startsWith("1.")) {
// If provider has as an "openid-configuration" well-known endpoint
// or an "openid" scope request, it will also likely be able to receive an `id_token`
// Only do this if this function is not called with user options to avoid overriding in later stage.
normalized.idToken = Boolean(
normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ??
normalized.authorization?.params?.scope?.includes("openid")
)
if (!normalized.checks) normalized.checks = ["state"]
}
return normalized
}

44
node_modules/next-auth/src/core/lib/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,44 @@
import { createHash } from "crypto"
import type { AuthOptions } from "../.."
import type { InternalOptions } from "../types"
import type { InternalUrl } from "../../utils/parse-url"
/**
* Takes a number in seconds and returns the date in the future.
* Optionally takes a second date parameter. In that case
* the date in the future will be calculated from that date instead of now.
*/
export function fromDate(time: number, date = Date.now()) {
return new Date(date + time * 1000)
}
export function hashToken(token: string, options: InternalOptions<"email">) {
const { provider, secret } = options
return (
createHash("sha256")
// Prefer provider specific secret, but use default secret if none specified
.update(`${token}${provider.secret ?? secret}`)
.digest("hex")
)
}
/**
* Secret used salt cookies and tokens (e.g. for CSRF protection).
* If no secret option is specified then it creates one on the fly
* based on options passed here. If options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent. If no secret provided in production, we throw an error. */
export function createSecret(params: {
authOptions: AuthOptions
url: InternalUrl
}) {
const { authOptions, url } = params
return (
authOptions.secret ??
// TODO: Remove falling back to default secret, and error in dev if one isn't provided
createHash("sha256")
.update(JSON.stringify({ ...url, ...authOptions }))
.digest("hex")
)
}

112
node_modules/next-auth/src/core/pages/error.tsx generated vendored Normal file
View File

@@ -0,0 +1,112 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"
/**
* The following errors are passed as error query parameters to the default or overridden error page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#error-page) */
export type ErrorType =
| "default"
| "configuration"
| "accessdenied"
| "verification"
export interface ErrorProps {
url?: InternalUrl
theme?: Theme
error?: ErrorType
}
interface ErrorView {
status: number
heading: string
message: JSX.Element
signin?: JSX.Element
}
/** Renders an error page. */
export default function ErrorPage(props: ErrorProps) {
const { url, error = "default", theme } = props
const signinPageUrl = `${url}/signin`
const errors: Record<ErrorType, ErrorView> = {
default: {
status: 200,
heading: "Error",
message: (
<p>
<a className="site" href={url?.origin}>
{url?.host}
</a>
</p>
),
},
configuration: {
status: 500,
heading: "Server error",
message: (
<div>
<p>There is a problem with the server configuration.</p>
<p>Check the server logs for more information.</p>
</div>
),
},
accessdenied: {
status: 403,
heading: "Access Denied",
message: (
<div>
<p>You do not have permission to sign in.</p>
<p>
<a className="button" href={signinPageUrl}>
Sign in
</a>
</p>
</div>
),
},
verification: {
status: 403,
heading: "Unable to sign in",
message: (
<div>
<p>The sign in link is no longer valid.</p>
<p>It may have been used already or it may have expired.</p>
</div>
),
signin: (
<a className="button" href={signinPageUrl}>
Sign in
</a>
),
},
}
const { status, heading, message, signin } =
errors[error.toLowerCase()] ?? errors.default
return {
status,
html: (
<div className="error">
{theme?.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme?.brandColor}
}
`,
}}
/>
)}
<div className="card">
{theme?.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<h1>{heading}</h1>
<div className="message">{message}</div>
{signin}
</div>
</div>
),
}
}

79
node_modules/next-auth/src/core/pages/index.ts generated vendored Normal file
View File

@@ -0,0 +1,79 @@
import renderToString from "preact-render-to-string"
import SigninPage from "./signin"
import SignoutPage from "./signout"
import VerifyRequestPage from "./verify-request"
import ErrorPage from "./error"
import css from "../../css"
import type { InternalOptions } from "../types"
import type { RequestInternal, ResponseInternal } from ".."
import type { Cookie } from "../lib/cookie"
import type { ErrorType } from "./error"
type RenderPageParams = {
query?: RequestInternal["query"]
cookies?: Cookie[]
} & Partial<
Pick<
InternalOptions,
"url" | "callbackUrl" | "csrfToken" | "providers" | "theme"
>
>
/**
* Unless the user defines their [own pages](https://next-auth.js.org/configuration/pages),
* we render a set of default ones, using Preact SSR.
*/
export default function renderPage(params: RenderPageParams) {
const { url, theme, query, cookies } = params
function send({ html, title, status }: any): ResponseInternal {
return {
cookies,
status,
headers: [{ key: "Content-Type", value: "text/html" }],
body: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${
theme?.colorScheme ?? "auto"
}"><div class="page">${renderToString(html)}</div></body></html>`,
}
}
return {
signin(props?: any) {
return send({
html: SigninPage({
csrfToken: params.csrfToken,
providers: params.providers,
callbackUrl: params.callbackUrl,
theme,
...query,
...props,
}),
title: "Sign In",
})
},
signout(props?: any) {
return send({
html: SignoutPage({
csrfToken: params.csrfToken,
url,
theme,
...props,
}),
title: "Sign Out",
})
},
verifyRequest(props?: any) {
return send({
html: VerifyRequestPage({ url, theme, ...props }),
title: "Verify Request",
})
},
error(props?: { error?: ErrorType }) {
return send({
...ErrorPage({ url, theme, ...props }),
title: "Error",
})
},
}
}

279
node_modules/next-auth/src/core/pages/signin.tsx generated vendored Normal file
View File

@@ -0,0 +1,279 @@
import type { InternalProvider, Theme } from "../types"
import type React from "react"
/**
* The following errors are passed as error query parameters to the default or overridden sign-in page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#sign-in-page) */
export type SignInErrorTypes =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
| "OAuthAccountNotLinked"
| "EmailSignin"
| "CredentialsSignin"
| "SessionRequired"
| "default"
export interface SignInServerPageParams {
csrfToken: string
providers: InternalProvider[]
callbackUrl: string
email: string
error: SignInErrorTypes
theme: Theme
}
function hexToRgba(hex?: string, alpha = 1) {
if (!hex) {
return
}
// Remove the "#" character if it's included
hex = hex.replace(/^#/, "")
// Expand 3-digit hex codes to their 6-digit equivalents
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
// Parse the hex value to separate R, G, and B components
const bigint = parseInt(hex, 16)
const r = (bigint >> 16) & 255
const g = (bigint >> 8) & 255
const b = bigint & 255
// Ensure the alpha value is within the valid range [0, 1]
alpha = Math.min(Math.max(alpha, 0), 1)
// Construct the RGBA string
const rgba = `rgba(${r}, ${g}, ${b}, ${alpha})`
return rgba
}
export default function SigninPage(props: SignInServerPageParams) {
const {
csrfToken,
providers,
callbackUrl,
theme,
email,
error: errorType,
} = props
// We only want to render providers
const providersToRender = providers.filter((provider) => {
if (provider.type === "oauth" || provider.type === "email") {
// Always render oauth and email type providers
return true
} else if (provider.type === "credentials" && provider.credentials) {
// Only render credentials type provider if credentials are defined
return true
}
// Don't render other provider types
return false
})
if (typeof document !== "undefined" && theme.buttonText) {
document.documentElement.style.setProperty(
"--button-text-color",
theme.buttonText
)
}
if (typeof document !== "undefined" && theme.brandColor) {
document.documentElement.style.setProperty(
"--brand-color",
theme.brandColor
)
}
const errors: Record<SignInErrorTypes, string> = {
Signin: "Try signing in with a different account.",
OAuthSignin: "Try signing in with a different account.",
OAuthCallback: "Try signing in with a different account.",
OAuthCreateAccount: "Try signing in with a different account.",
EmailCreateAccount: "Try signing in with a different account.",
Callback: "Try signing in with a different account.",
OAuthAccountNotLinked:
"To confirm your identity, sign in with the same account you used originally.",
EmailSignin: "The e-mail could not be sent.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
}
const error = errorType && (errors[errorType] ?? errors.default)
const providerLogoPath = "https://authjs.dev/img/providers"
return (
<div className="signin">
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/>
)}
{theme.buttonText && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--button-text-color: ${theme.buttonText}
}
`,
}}
/>
)}
<div className="card">
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
{error && (
<div className="error">
<p>{error}</p>
</div>
)}
{providersToRender.map((provider, i: number) => {
let bg, text, logo, logoDark, bgDark, textDark
if (provider.type === "oauth") {
;({
bg = "",
text = "",
logo = "",
bgDark = bg,
textDark = text,
logoDark = "",
} = provider.style ?? {})
logo = logo.startsWith("/")
? `${providerLogoPath}${logo as string}`
: logo
logoDark = logoDark.startsWith("/")
? `${providerLogoPath}${logoDark as string}`
: logoDark || logo
logoDark ||= logo
}
return (
<div key={provider.id} className="provider">
{provider.type === "oauth" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{callbackUrl && (
<input
type="hidden"
name="callbackUrl"
value={callbackUrl}
/>
)}
<button
type="submit"
className="button"
style={
// eslint-disable-next-line
{
"--provider-bg": bg,
"--provider-dark-bg": bgDark,
"--provider-color": text,
"--provider-dark-color": textDark,
"--provider-bg-hover": hexToRgba(bg, 0.8),
"--provider-dark-bg-hover": hexToRgba(bgDark, 0.8),
} as React.CSSProperties
}
>
{logo && (
<img
loading="lazy"
height={24}
width={24}
id="provider-logo"
src={`${
logo.startsWith("/") ? providerLogoPath : ""
}${logo}`}
/>
)}
{logoDark && (
<img
loading="lazy"
height={24}
width={24}
id="provider-logo-dark"
src={`${
logo.startsWith("/") ? providerLogoPath : ""
}${logoDark}`}
/>
)}
<span>Sign in with {provider.name}</span>
</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i > 0 &&
providersToRender[i - 1].type !== "email" &&
providersToRender[i - 1].type !== "credentials" && <hr />}
{provider.type === "email" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
<label
className="section-header"
htmlFor={`input-email-for-${provider.id}-provider`}
>
Email
</label>
<input
id={`input-email-for-${provider.id}-provider`}
autoFocus
type="email"
name="email"
value={email}
placeholder="email@example.com"
required
/>
<button id="submitButton" type="submit">
Sign in with {provider.name}
</button>
</form>
)}
{provider.type === "credentials" && (
<form action={provider.callbackUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{Object.keys(provider.credentials).map((credential) => {
return (
<div key={`input-group-${provider.id}`}>
<label
className="section-header"
htmlFor={`input-${credential}-for-${provider.id}-provider`}
>
{provider.credentials[credential].label ?? credential}
</label>
<input
name={credential}
id={`input-${credential}-for-${provider.id}-provider`}
type={provider.credentials[credential].type ?? "text"}
placeholder={
provider.credentials[credential].placeholder ?? ""
}
{...provider.credentials[credential]}
/>
</div>
)
})}
<button type="submit">Sign in with {provider.name}</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i + 1 < providersToRender.length && <hr />}
</div>
)
})}
</div>
</div>
)
}

48
node_modules/next-auth/src/core/pages/signout.tsx generated vendored Normal file
View File

@@ -0,0 +1,48 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"
export interface SignoutProps {
url: InternalUrl
csrfToken: string
theme: Theme
}
export default function SignoutPage(props: SignoutProps) {
const { url, csrfToken, theme } = props
return (
<div className="signout">
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/>
)}
{theme.buttonText && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--button-text-color: ${theme.buttonText}
}
`,
}}
/>
)}
<div className="card">
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<h1>Signout</h1>
<p>Are you sure you want to sign out?</p>
<form action={`${url}/signout`} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
<button id="submitButton" type="submit">Sign out</button>
</form>
</div>
</div>
)
}

View File

@@ -0,0 +1,37 @@
import { Theme } from "../.."
import { InternalUrl } from "../../utils/parse-url"
interface VerifyRequestPageProps {
url: InternalUrl
theme: Theme
}
export default function VerifyRequestPage(props: VerifyRequestPageProps) {
const { url, theme } = props
return (
<div className="verify-request">
{theme.brandColor && (
<style
dangerouslySetInnerHTML={{
__html: `
:root {
--brand-color: ${theme.brandColor}
}
`,
}}
/>
)}
<div className="card">
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
<h1>Check your email</h1>
<p>A sign in link has been sent to your email address.</p>
<p>
<a className="site" href={url.origin}>
{url.host}
</a>
</p>
</div>
</div>
)
}

429
node_modules/next-auth/src/core/routes/callback.ts generated vendored Normal file
View File

@@ -0,0 +1,429 @@
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import { hashToken } from "../lib/utils"
import getAdapterUserFromEmail from "../lib/email/getUserFromEmail"
import type { InternalOptions } from "../types"
import type { RequestInternal, ResponseInternal } from ".."
import type { Cookie, SessionStore } from "../lib/cookie"
import type { User } from "../.."
import type { AdapterSession } from "../../adapters"
/** Handle callbacks from login services */
export default async function callback(params: {
options: InternalOptions
query: RequestInternal["query"]
method: Required<RequestInternal>["method"]
body: RequestInternal["body"]
headers: RequestInternal["headers"]
cookies: RequestInternal["cookies"]
sessionStore: SessionStore
}): Promise<ResponseInternal> {
const { options, query, body, method, headers, sessionStore } = params
const {
provider,
adapter,
url,
callbackUrl,
pages,
jwt,
events,
callbacks,
session: { strategy: sessionStrategy, maxAge: sessionMaxAge },
logger,
} = options
const cookies: Cookie[] = []
const useJwtSession = sessionStrategy === "jwt"
if (provider.type === "oauth") {
try {
const {
profile,
account,
OAuthProfile,
cookies: oauthCookies,
} = await oAuthCallback({
query,
body,
method,
options,
cookies: params.cookies,
})
if (oauthCookies.length) cookies.push(...oauthCookies)
try {
// Make it easier to debug when adding a new provider
logger.debug("OAUTH_CALLBACK_RESPONSE", {
profile,
account,
OAuthProfile,
})
// If we don't have a profile object then either something went wrong
// or the user cancelled signing in. We don't know which, so we just
// direct the user to the signin page for now. We could do something
// else in future.
//
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile || !account || !OAuthProfile) {
return { redirect: `${url}/signin`, cookies }
}
// Check if user is allowed to sign in
// Attempt to get Profile from OAuth provider details before invoking
// signIn callback - but if no user object is returned, that is fine
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: provider.id,
})
if (userByAccount) userOrProfile = userByAccount
}
try {
const isAllowed = await callbacks.signIn({
user: userOrProfile,
account,
profile: OAuthProfile,
})
if (!isAllowed) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
} else if (typeof isAllowed === "string") {
return { redirect: isAllowed, cookies }
}
} catch (error) {
return {
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message,
)}`,
cookies,
}
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
profile,
account,
options,
})
if (useJwtSession) {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
profile: OAuthProfile,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
} else {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
},
})
}
// @ts-expect-error
await events.signIn?.({ user, account, profile, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
cookies,
}
}
// Callback URL is already verified at this point, so safe to use if specified
return { redirect: callbackUrl, cookies }
} catch (error) {
if ((error as Error).name === "AccountNotLinkedError") {
// If the email on the account is already linked, but not with this OAuth account
return {
redirect: `${url}/error?error=OAuthAccountNotLinked`,
cookies,
}
} else if ((error as Error).name === "CreateUserError") {
return { redirect: `${url}/error?error=OAuthCreateAccount`, cookies }
}
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error as Error)
return { redirect: `${url}/error?error=Callback`, cookies }
}
} catch (error) {
if ((error as Error).name === "OAuthCallbackError") {
logger.error("OAUTH_CALLBACK_ERROR", {
error: error as Error,
providerId: provider.id,
})
return { redirect: `${url}/error?error=OAuthCallback`, cookies }
}
logger.error("OAUTH_CALLBACK_ERROR", error as Error)
return { redirect: `${url}/error?error=Callback`, cookies }
}
} else if (provider.type === "email") {
try {
const paramToken = query?.token as string | undefined
const paramIdentifier = query?.email as string | undefined
// If token is missing, the sign-in URL was manually opened without this param or the `sendVerificationRequest` method did not send the link correctly in the email.
if (!paramToken) {
return { redirect: `${url}/error?error=configuration`, cookies }
}
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
const invite = await adapter.useVerificationToken({
// @ts-expect-error User-land adapters might decide to omit the identifier during lookup
identifier: paramIdentifier,
token: hashToken(paramToken, options),
})
const invalidInvite =
!invite ||
invite.expires.valueOf() < Date.now() ||
// The user might have configured the link to not contain the identifier
// so we only compare if it exists
(paramIdentifier && invite.identifier !== paramIdentifier)
if (invalidInvite) {
return { redirect: `${url}/error?error=Verification`, cookies }
}
const profile = await getAdapterUserFromEmail({
email: invite.identifier,
adapter,
})
const account = {
providerAccountId: profile.email,
type: "email" as const,
provider: provider.id,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
user: profile,
account,
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
} else if (typeof signInCallbackResponse === "string") {
return { redirect: signInCallbackResponse, cookies }
}
} catch (error) {
return {
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message,
)}`,
cookies,
}
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
profile,
account,
options,
})
if (useJwtSession) {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
} else {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
},
})
}
await events.signIn?.({ user, account, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
cookies,
}
}
// Callback URL is already verified at this point, so safe to use if specified
return { redirect: callbackUrl, cookies }
} catch (error) {
if ((error as Error).name === "CreateUserError") {
return { redirect: `${url}/error?error=EmailCreateAccount`, cookies }
}
logger.error("CALLBACK_EMAIL_ERROR", error as Error)
return { redirect: `${url}/error?error=Callback`, cookies }
}
} else if (provider.type === "credentials" && method === "POST") {
const credentials = body
let user: User | null
try {
user = await provider.authorize(credentials, {
query,
body,
headers,
method,
})
if (!user) {
return {
status: 401,
redirect: `${url}/error?${new URLSearchParams({
error: "CredentialsSignin",
provider: provider.id,
})}`,
cookies,
}
}
} catch (error) {
return {
status: 401,
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message,
)}`,
cookies,
}
}
/** @type {import("src").Account} */
const account = {
providerAccountId: user.id,
type: "credentials",
provider: provider.id,
}
try {
const isAllowed = await callbacks.signIn({
user,
// @ts-expect-error
account,
credentials,
})
if (!isAllowed) {
return {
status: 403,
redirect: `${url}/error?error=AccessDenied`,
cookies,
}
} else if (typeof isAllowed === "string") {
return { redirect: isAllowed, cookies }
}
} catch (error) {
return {
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message,
)}`,
cookies,
}
}
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
isNewUser: false,
trigger: "signIn",
})
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
// @ts-expect-error
await events.signIn?.({ user, account })
return { redirect: callbackUrl, cookies }
}
return {
status: 500,
body: `Error: Callback for provider type ${provider.type} not supported`,
cookies,
}
}

5
node_modules/next-auth/src/core/routes/index.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export { default as callback } from './callback'
export { default as signin } from './signin'
export { default as signout } from './signout'
export { default as session } from './session'
export { default as providers } from './providers'

30
node_modules/next-auth/src/core/routes/providers.ts generated vendored Normal file
View File

@@ -0,0 +1,30 @@
import type { ResponseInternal } from ".."
import type { InternalProvider } from "../types"
export interface PublicProvider {
id: string
name: string
type: string
signinUrl: string
callbackUrl: string
}
/**
* Return a JSON object with a list of all OAuth providers currently configured
* and their signin and callback URLs. This makes it possible to automatically
* generate buttons for all providers when rendering client side.
*/
export default function providers(
providers: InternalProvider[]
): ResponseInternal<Record<string, PublicProvider>> {
return {
headers: [{ key: "Content-Type", value: "application/json" }],
body: providers.reduce<Record<string, PublicProvider>>(
(acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
},
{}
),
}
}

191
node_modules/next-auth/src/core/routes/session.ts generated vendored Normal file
View File

@@ -0,0 +1,191 @@
import { fromDate } from "../lib/utils"
import type { InternalOptions } from "../types"
import type { ResponseInternal } from ".."
import type { Session } from "../.."
import type { SessionStore } from "../lib/cookie"
interface SessionParams {
options: InternalOptions
sessionStore: SessionStore
isUpdate?: boolean
newSession?: any
}
/**
* Return a session object (without any private fields)
* for Single Page App clients
*/
export default async function session(
params: SessionParams
): Promise<ResponseInternal<Session | {}>> {
const { options, sessionStore, newSession, isUpdate } = params
const {
adapter,
jwt,
events,
callbacks,
logger,
session: { strategy: sessionStrategy, maxAge: sessionMaxAge },
} = options
const response: ResponseInternal<Session | {}> = {
body: {},
headers: [
{ key: "Content-Type", value: "application/json" },
...(isUpdate
? []
: [
{
key: "Cache-Control",
value: "private, no-cache, no-store",
},
{
key: "Pragma",
value: "no-cache",
},
{
key: "Expires",
value: "0",
},
]),
].filter(Boolean),
cookies: [],
}
const sessionToken = sessionStore.value
if (!sessionToken) return response
if (sessionStrategy === "jwt") {
try {
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
if (!decodedToken) throw new Error("JWT invalid")
// @ts-expect-error
const token = await callbacks.jwt({
token: decodedToken,
...(isUpdate && { trigger: "update" }),
session: newSession,
})
const newExpires = fromDate(sessionMaxAge)
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
// @ts-expect-error Property 'user' is missing in type
const updatedSession = await callbacks.session({
session: {
user: {
name: decodedToken?.name,
email: decodedToken?.email,
image: decodedToken?.picture,
},
expires: newExpires.toISOString(),
},
token,
})
// Return session payload as response
response.body = updatedSession
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newToken = await jwt.encode({
...jwt,
token,
maxAge: options.session.maxAge,
})
// Set cookie, to also update expiry date on cookie
const sessionCookies = sessionStore.chunk(newToken, {
expires: newExpires,
})
response.cookies?.push(...sessionCookies)
await events.session?.({ session: updatedSession, token })
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error("JWT_SESSION_ERROR", error as Error)
response.cookies?.push(...sessionStore.clean())
}
} else {
try {
// @ts-expect-error -- adapter is checked to be defined in `init`
const { getSessionAndUser, deleteSession, updateSession } = adapter
let userAndSession = await getSessionAndUser(sessionToken)
// If session has expired, clean up the database
if (
userAndSession &&
userAndSession.session.expires.valueOf() < Date.now()
) {
await deleteSession(sessionToken)
userAndSession = null
}
if (userAndSession) {
const { user, session } = userAndSession
const sessionUpdateAge = options.session.updateAge
// Calculate last updated date to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
const sessionIsDueToBeUpdatedDate =
session.expires.valueOf() -
sessionMaxAge * 1000 +
sessionUpdateAge * 1000
const newExpires = fromDate(sessionMaxAge)
// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (sessionIsDueToBeUpdatedDate <= Date.now()) {
await updateSession({ sessionToken, expires: newExpires })
}
// Pass Session through to the session callback
// @ts-expect-error Property 'token' is missing in type
const sessionPayload = await callbacks.session({
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
session: {
user: { name: user.name, email: user.email, image: user.image },
expires: session.expires.toISOString(),
},
user,
newSession,
...(isUpdate ? { trigger: "update" } : {}),
})
// Return session payload as response
response.body = sessionPayload
// Set cookie again to update expiry
response.cookies?.push({
name: options.cookies.sessionToken.name,
value: sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: newExpires,
},
})
// @ts-expect-error
await events.session?.({ session: sessionPayload })
} else if (sessionToken) {
// If `sessionToken` was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
response.cookies?.push(...sessionStore.clean())
}
} catch (error) {
logger.error("SESSION_ERROR", error as Error)
}
}
return response
}

126
node_modules/next-auth/src/core/routes/signin.ts generated vendored Normal file
View File

@@ -0,0 +1,126 @@
import getAuthorizationUrl from "../lib/oauth/authorization-url"
import emailSignin from "../lib/email/signin"
import getAdapterUserFromEmail from "../lib/email/getUserFromEmail"
import type { RequestInternal, ResponseInternal } from ".."
import type { InternalOptions } from "../types"
import type { Account } from "../.."
/** Handle requests to /api/auth/signin */
export default async function signin(params: {
options: InternalOptions<"oauth" | "email">
query: RequestInternal["query"]
body: RequestInternal["body"]
}): Promise<ResponseInternal> {
const { options, query, body } = params
const { url, callbacks, logger, provider } = options
if (!provider.type) {
return {
status: 500,
// @ts-expect-error
text: `Error: Type not specified for ${provider.name}`,
}
}
if (provider.type === "oauth") {
try {
const response = await getAuthorizationUrl({ options, query })
return response
} catch (error) {
logger.error("SIGNIN_OAUTH_ERROR", {
error: error as Error,
providerId: provider.id,
})
return { redirect: `${url}/error?error=OAuthSignin` }
}
} else if (provider.type === "email") {
let email: string = body?.email
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
const normalizer: (identifier: string) => string =
provider.normalizeIdentifier ??
((identifier) => {
const trimmedEmail = identifier.trim()
// Validate email format according to RFC 5321/5322
// Reject emails with quotes in the local part to prevent address parser exploits
// Reject multiple @ symbols which could indicate an exploit attempt
const atCount = (trimmedEmail.match(/@/g) ?? []).length
if (atCount !== 1) {
throw new Error("Invalid email address format.")
}
// Check for quotes in the email address which could be used for exploits
if (trimmedEmail.includes('"')) {
throw new Error("Invalid email address format.")
}
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = trimmedEmail.toLowerCase().split("@")
// Validate that both local and domain parts exist and are non-empty
if (!local || !domain) {
throw new Error("Invalid email address format.")
}
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
// Additional validation: domain must have at least one dot
if (!domain.includes(".")) {
throw new Error("Invalid email address format.")
}
return `${local}@${domain}`
})
try {
email = normalizer(body?.email)
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
const user = await getAdapterUserFromEmail({
email,
adapter: options.adapter,
})
const account: Account = {
providerAccountId: email,
userId: email,
type: "email",
provider: provider.id,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
user,
account,
email: { verificationRequest: true },
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied` }
} else if (typeof signInCallbackResponse === "string") {
return { redirect: signInCallbackResponse }
}
} catch (error) {
return {
redirect: `${url}/error?${new URLSearchParams({
error: error as string,
})}`,
}
}
try {
const redirect = await emailSignin(email, options)
return { redirect }
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
}
return { redirect: `${url}/signin` }
}

45
node_modules/next-auth/src/core/routes/signout.ts generated vendored Normal file
View File

@@ -0,0 +1,45 @@
import type { InternalOptions } from "../types"
import type { ResponseInternal } from ".."
import type { SessionStore } from "../lib/cookie"
/** Handle requests to /api/auth/signout */
export default async function signout(params: {
options: InternalOptions
sessionStore: SessionStore
}): Promise<ResponseInternal> {
const { options, sessionStore } = params
const { adapter, events, jwt, callbackUrl, logger, session } = options
const sessionToken = sessionStore?.value
if (!sessionToken) {
return { redirect: callbackUrl }
}
if (session.strategy === "jwt") {
// Dispatch signout event
try {
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
// @ts-expect-error
await events.signOut?.({ token: decodedJwt })
} catch (error) {
// Do nothing if decoding the JWT fails
logger.error("SIGNOUT_ERROR", error)
}
} else {
try {
// @ts-expect-error -- adapter is checked to be defined in `init`
const session = await adapter.deleteSession(sessionToken)
// Dispatch signout event
// @ts-expect-error
await events.signOut?.({ session })
} catch (error) {
// If error, log it but continue
logger.error("SIGNOUT_ERROR", error as Error)
}
}
// Remove Session Token
const sessionCookies = sessionStore.clean()
return { redirect: callbackUrl, cookies: sessionCookies }
}

626
node_modules/next-auth/src/core/types.ts generated vendored Normal file
View File

@@ -0,0 +1,626 @@
import type { Adapter, AdapterUser } from "../adapters"
import type {
Provider,
CredentialInput,
ProviderType,
EmailConfig,
CredentialsConfig,
OAuthConfig,
AuthorizationEndpointHandler,
TokenEndpointHandler,
UserinfoEndpointHandler,
} from "../providers"
import type { TokenSetParameters } from "openid-client"
import type { JWT, JWTOptions } from "../jwt"
import type { LoggerInstance } from "../utils/logger"
import type { CookieSerializeOptions } from "cookie"
import type { NextApiRequest, NextApiResponse } from "next"
import type { InternalUrl } from "../utils/parse-url"
export type Awaitable<T> = T | PromiseLike<T>
export type { LoggerInstance }
/**
* Configure your NextAuth instance
*
* [Documentation](https://next-auth.js.org/configuration/options#options)
*/
export interface AuthOptions {
/**
* An array of authentication providers for signing in
* (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order.
* This can be one of the built-in providers or an object with a custom provider.
* * **Default value**: `[]`
* * **Required**: *Yes*
*
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://providers.authjs.dev)
*/
providers: Provider[]
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment variables.
* Otherwise, it will use a hash of all configuration options, including Client ID / Secrets for entropy.
*
* NOTE: The last behavior is extremely volatile, and will throw an error in production.
* * **Default value**: `string` (SHA hash of the "options" object)
* * **Required**: No - **but strongly recommended**!
*
* [Documentation](https://next-auth.js.org/configuration/options#secret)
*/
secret?: string
/**
* Configure your session settings, such as determining whether to use JWT or a database,
* setting the idle session expiration duration, or implementing write operation throttling for database usage.
* * **Default value**: See the documentation page
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#session)
*/
session?: Partial<SessionOptions>
/**
* JSON Web Tokens are enabled by default if you have not specified an adapter.
* JSON Web Tokens are encrypted (JWE) by default. We recommend you keep this behaviour.
* * **Default value**: See the documentation page
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#jwt)
*/
jwt?: Partial<JWTOptions>
/**
* Specify URLs to be used if you want to create custom sign in, sign out and error pages.
* Pages specified will override the corresponding built-in page.
* * **Default value**: `{}`
* * **Required**: *No*
* @example
*
* ```js
* pages: {
* signIn: '/auth/signin',
* signOut: '/auth/signout',
* error: '/auth/error',
* verifyRequest: '/auth/verify-request',
* newUser: '/auth/new-user'
* }
* ```
*
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages)
*/
pages?: Partial<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 documentation
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: Partial<CallbacksOptions>
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
* The content of the message object varies depending on the flow
* (e.g. OAuth or Email authentication flow, JWT or database sessions, etc),
* but typically contains a user object and/or contents of the JSON Web Token
* and other information relevant to the event.
* * **Default value**: `{}`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
*/
events?: Partial<EventCallbacks>
/**
* You can use the adapter option to pass in your database adapter.
*
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Adapters Overview](https://next-auth.js.org/adapters/overview)
*/
adapter?: Adapter
/**
* Set debug to true to enable debug messages for authentication and database operations.
* * **Default value**: `false`
* * **Required**: *No*
*
* - ⚠ If you added a custom `logger`, this setting is ignored.
*
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentation](https://next-auth.js.org/configuration/options#logger)
*/
debug?: boolean
/**
* Override any of the logger levels (`undefined` levels will use the built-in logger),
* and intercept logs in NextAuth. You can use this option to send NextAuth logs to a third-party logging service.
* * **Default value**: `console`
* * **Required**: *No*
*
* @example
*
* ```js
* // /pages/api/auth/[...nextauth].js
* import log from "logging-service"
* export default NextAuth({
* logger: {
* error(code, ...message) {
* log.error(code, message)
* },
* warn(code, ...message) {
* log.warn(code, message)
* },
* debug(code, ...message) {
* log.debug(code, message)
* }
* }
* })
* ```
*
* - ⚠ When set, the `debug` option is ignored
*
* [Documentation](https://next-auth.js.org/configuration/options#logger) |
* [Debug documentation](https://next-auth.js.org/configuration/options#debug)
*/
logger?: Partial<LoggerInstance>
/**
* Changes the theme of pages.
* Set to `"light"` if you want to force pages to always be light.
* Set to `"dark"` if you want to force pages to always be dark.
* Set to `"auto"`, (or leave this option out)if you want the pages to follow the preferred system theme.
* * **Default value**: `"auto"`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
*/
theme?: Theme
/**
* 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.
* 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
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#usesecurecookies)
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*/
useSecureCookies?: boolean
/**
* 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 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**: `{}`
* * **Required**: No
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: Partial<CookiesOptions>
}
/**
* Change the theme of the built-in pages.
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
* [Pages](https://next-auth.js.org/configuration/pages)
*/
export interface Theme {
colorScheme?: "auto" | "dark" | "light"
logo?: string
brandColor?: string
buttonText?: string
}
/**
* Different tokens returned by OAuth Providers.
* Some of them are available with different casing,
* but they refer to the same value.
*/
export type TokenSet = TokenSetParameters
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends Partial<TokenSet> {
/**
* This value depends on the type of the provider being used to create the account.
* - oauth: The OAuth account's id, returned from the `profile()` callback.
* - email: The user's email address.
* - credentials: `id` returned from the `authorize()` callback
*/
providerAccountId: string
/** id of the user this account belongs to. */
userId?: string
/** id of the provider used for this account */
provider: string
/** Provider's type for this account */
type: ProviderType
}
/** The OAuth profile returned from your provider */
export interface Profile {
sub?: string
name?: string
email?: string
image?: string
}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<P = Profile, A = 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: (params: {
user: User | AdapterUser
account: A | null
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile?: P
/**
* If Email provider is used, on the first call, it contains a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
* When the callback is invoked after a user has clicked on a sign in link,
* this property will not be present. You can check for the `verificationRequest` property
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email?: {
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
credentials?: Record<string, CredentialInput>
}) => 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: (params: {
/** URL provided as callback URL by the client */
url: string
/** Default base URL of site (can be used as fallback) */
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 (email, name, image)
* 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 explicitly 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: (
params:
| {
session: Session
/** Available when {@link SessionOptions.strategy} is set to `"jwt"` */
token: JWT
/** Available when {@link SessionOptions.strategy} is set to `"database"`. */
user: AdapterUser
} & {
/**
* Available when using {@link SessionOptions.strategy} `"database"`, this is the data
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
*
* ⚠ Note, you should validate this data before using it.
*/
newSession: any
trigger: "update"
}
) => Awaitable<Session | DefaultSession>
/**
* 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.
*
* The JWT is encrypted by default.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt: (
// TODO: remove in `@auth/core` in favor of `trigger: "signUp"`
params: {
/**
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT},
* `name`, `email` and `picture` will be included.
*
* Otherwise, it will be the full {@link JWT} for subsequent calls.
*/
token: JWT
/**
* Either the result of the {@link OAuthConfig.profile} or the {@link CredentialsConfig.authorize} callback.
* @note available when `trigger` is `"signIn"` or `"signUp"`.
*
* Resources:
* - [Credentials Provider](https://next-auth.js.org/providers/credentials)
* - [User database model](https://authjs.dev/reference/adapters#user)
*/
user: User | AdapterUser
/**
* Contains information about the provider that was used to sign in.
* Also includes {@link TokenSet}
* @note available when `trigger` is `"signIn"` or `"signUp"`
*/
account: A | null
/**
* The OAuth profile returned from your provider.
* (In case of OIDC it will be the decoded ID Token or /userinfo response)
* @note available when `trigger` is `"signIn"`.
*/
profile?: P
/**
* Check why was the jwt callback invoked. Possible reasons are:
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present.
* - user sign-up: a user is created for the first time in the database (when {@link SessionOptions.strategy} is set to `"database"`})
* - update event: Triggered by the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
* In case of the latter, `trigger` will be `undefined`.
*/
trigger?: "signIn" | "signUp" | "update"
/** @deprecated use `trigger === "signUp"` instead */
isNewUser?: boolean
/**
* When using {@link SessionOptions.strategy} `"jwt"`, this is the data
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
*
* ⚠ Note, you should validate this data before using it.
*/
session?: any
}
) => Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookieOption {
name: string
options: CookieSerializeOptions
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
state: CookieOption
nonce: CookieOption
}
/**
* The various event callbacks you can register for from next-auth
*
* [Documentation](https://next-auth.js.org/configuration/events)
*/
export interface EventCallbacks {
/**
* If using a `credentials` type auth, the user is the raw response from your
* credential provider.
* For other providers, you'll get the User object from your adapter, the account,
* and an indicator if the user was new to your Adapter.
*/
signIn: (message: {
user: User
account: Account | null
profile?: Profile
isNewUser?: boolean
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `session`: The session object from your adapter that is being ended.
*/
signOut: (message: { session: Session; token: JWT }) => Awaitable<void>
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User | AdapterUser
account: Account
profile: User | AdapterUser
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `session`: The session object from your adapter.
*/
session: (message: { session: Session; token: JWT }) => Awaitable<void>
}
export type EventType = keyof EventCallbacks
/** [Documentation](https://next-auth.js.org/configuration/pages) */
export interface PagesOptions {
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
}
export type ISODateString = string
export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
image?: string | null
}
expires: ISODateString
}
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `SessionProvider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends DefaultSession {}
export type SessionStrategy = "jwt" | "database"
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
export interface SessionOptions {
/**
* Choose how you want to save the user session.
* The default is `"jwt"`, an encrypted JWT (JWE) in the session cookie.
*
* If you use an `adapter` however, we default it to `"database"` instead.
* You can still force a JWT session by explicitly defining `"jwt"`.
*
* When using `"database"`, the session cookie will only contain a `sessionToken` value,
* which is used to look up the session in the database.
*
* [Documentation](https://next-auth.js.org/configuration/options#session) | [Adapter](https://next-auth.js.org/configuration/options#adapter) | [About JSON Web Tokens](https://next-auth.js.org/faq#json-web-tokens)
*/
strategy: SessionStrategy
/**
* Relative time from now in seconds when to expire the session
* @default 2592000 // 30 days
*/
maxAge: number
/**
* How often the session should be updated in seconds.
* If set to `0`, session is updated every time.
* @default 86400 // 1 day
*/
updateAge: number
/**
* Generate a custom session token for database-based sessions.
* By default, a random UUID or string is generated depending on the Node.js version.
* However, you can specify your own custom string (such as CUID) to be used.
* @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version
*/
generateSessionToken: () => Awaitable<string>
}
export interface DefaultUser {
id: string
name?: string | null
email?: string | null
image?: string | null
}
/**
* 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/oauth#using-a-custom-provider)
*/
export interface User extends DefaultUser {}
// Below are types that are only supposed be used by next-auth internally
/** @internal */
export interface OAuthConfigInternal<P>
extends Omit<OAuthConfig<P>, "authorization" | "token" | "userinfo"> {
authorization?: AuthorizationEndpointHandler
token?: TokenEndpointHandler
userinfo?: UserinfoEndpointHandler
}
/** @internal */
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? OAuthConfigInternal<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
? CredentialsConfig
: never) & {
signinUrl: string
callbackUrl: string
}
export type AuthAction =
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
| "_log"
type NonNullableFields<T> = {
[P in keyof T]-?: NonNullable<T[P]>
}
/** @internal */
export interface InternalOptions<TProviderType = ProviderType> {
providers: InternalProvider[]
/**
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` and `x-forwarded-proto` if the host is trusted.
* @default "http://localhost:3000/api/auth"
*/
url: InternalUrl
action: AuthAction
provider: InternalProvider<TProviderType>
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
theme: Theme
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
pages: Partial<PagesOptions>
jwt: JWTOptions
events: Partial<EventCallbacks>
adapter?: NonNullableFields<Adapter>
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
}
/** @internal */
export interface NextAuthRequest extends NextApiRequest {
options: InternalOptions
}
/** @internal */
export type NextAuthResponse<T = any> = NextApiResponse<T>
/** @internal */
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
export type NextAuthApiHandler<Result = void, Response = any> = (
req: NextAuthRequest,
res: NextAuthResponse<Response>
) => Awaitable<Result>