UNPKG

next-auth

Version:

Authentication for Next.js

337 lines (305 loc) 10.1 kB
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, } }