next-auth
Version:
Authentication for Next.js
337 lines (305 loc) • 10.1 kB
text/typescript
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,
}
}