Auto-commit 2026-04-29 16:31
This commit is contained in:
127
node_modules/next-auth/src/core/errors.ts
generated
vendored
Normal file
127
node_modules/next-auth/src/core/errors.ts
generated
vendored
Normal 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
336
node_modules/next-auth/src/core/index.ts
generated
vendored
Normal 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
155
node_modules/next-auth/src/core/init.ts
generated
vendored
Normal 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
149
node_modules/next-auth/src/core/lib/assert.ts
generated
vendored
Normal 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
233
node_modules/next-auth/src/core/lib/callback-handler.ts
generated
vendored
Normal 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
42
node_modules/next-auth/src/core/lib/callback-url.ts
generated
vendored
Normal 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
250
node_modules/next-auth/src/core/lib/cookie.ts
generated
vendored
Normal 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
55
node_modules/next-auth/src/core/lib/csrf-token.ts
generated
vendored
Normal 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 }
|
||||
}
|
||||
18
node_modules/next-auth/src/core/lib/default-callbacks.ts
generated
vendored
Normal file
18
node_modules/next-auth/src/core/lib/default-callbacks.ts
generated
vendored
Normal 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
|
||||
},
|
||||
}
|
||||
21
node_modules/next-auth/src/core/lib/email/getUserFromEmail.ts
generated
vendored
Normal file
21
node_modules/next-auth/src/core/lib/email/getUserFromEmail.ts
generated
vendored
Normal 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
51
node_modules/next-auth/src/core/lib/email/signin.ts
generated
vendored
Normal 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,
|
||||
})}`
|
||||
}
|
||||
63
node_modules/next-auth/src/core/lib/oauth/authorization-url.ts
generated
vendored
Normal file
63
node_modules/next-auth/src/core/lib/oauth/authorization-url.ts
generated
vendored
Normal 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
183
node_modules/next-auth/src/core/lib/oauth/callback.ts
generated
vendored
Normal 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
199
node_modules/next-auth/src/core/lib/oauth/checks.ts
generated
vendored
Normal 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
|
||||
},
|
||||
}
|
||||
73
node_modules/next-auth/src/core/lib/oauth/client-legacy.ts
generated
vendored
Normal file
73
node_modules/next-auth/src/core/lib/oauth/client-legacy.ts
generated
vendored
Normal 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
48
node_modules/next-auth/src/core/lib/oauth/client.ts
generated
vendored
Normal 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
93
node_modules/next-auth/src/core/lib/providers.ts
generated
vendored
Normal 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
44
node_modules/next-auth/src/core/lib/utils.ts
generated
vendored
Normal 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
112
node_modules/next-auth/src/core/pages/error.tsx
generated
vendored
Normal 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
79
node_modules/next-auth/src/core/pages/index.ts
generated
vendored
Normal 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
279
node_modules/next-auth/src/core/pages/signin.tsx
generated
vendored
Normal 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
48
node_modules/next-auth/src/core/pages/signout.tsx
generated
vendored
Normal 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>
|
||||
)
|
||||
}
|
||||
37
node_modules/next-auth/src/core/pages/verify-request.tsx
generated
vendored
Normal file
37
node_modules/next-auth/src/core/pages/verify-request.tsx
generated
vendored
Normal 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
429
node_modules/next-auth/src/core/routes/callback.ts
generated
vendored
Normal 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
5
node_modules/next-auth/src/core/routes/index.ts
generated
vendored
Normal 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
30
node_modules/next-auth/src/core/routes/providers.ts
generated
vendored
Normal 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
191
node_modules/next-auth/src/core/routes/session.ts
generated
vendored
Normal 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
126
node_modules/next-auth/src/core/routes/signin.ts
generated
vendored
Normal 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
45
node_modules/next-auth/src/core/routes/signout.ts
generated
vendored
Normal 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
626
node_modules/next-auth/src/core/types.ts
generated
vendored
Normal 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>
|
||||
Reference in New Issue
Block a user