From 2a3bb6b0b2b5e138bd45a4fc809bd5ba1ac02f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:07:02 +0100 Subject: [PATCH 1/7] fix(next-auth): revert to 4.17 and replay other fixes --- packages/next-auth/package.json | 2 +- packages/next-auth/src/client/_utils.ts | 6 +- packages/next-auth/src/core/errors.ts | 10 - packages/next-auth/src/core/index.ts | 151 ++++++------ packages/next-auth/src/core/init.ts | 41 ++-- packages/next-auth/src/core/lib/assert.ts | 17 +- packages/next-auth/src/core/lib/providers.ts | 7 +- packages/next-auth/src/core/lib/utils.ts | 14 +- packages/next-auth/src/core/pages/error.tsx | 3 +- packages/next-auth/src/core/pages/index.ts | 6 +- packages/next-auth/src/core/pages/signout.tsx | 3 +- .../next-auth/src/core/routes/callback.ts | 4 +- .../next-auth/src/core/routes/providers.ts | 6 +- packages/next-auth/src/core/routes/session.ts | 8 +- packages/next-auth/src/core/routes/signin.ts | 4 +- packages/next-auth/src/core/routes/signout.ts | 4 +- packages/next-auth/src/core/types.ts | 28 +-- packages/next-auth/src/index.ts | 4 +- packages/next-auth/src/jwt/index.ts | 2 +- packages/next-auth/src/next/index.ts | 153 ++++++------ packages/next-auth/src/next/middleware.ts | 70 ++---- packages/next-auth/src/next/utils.ts | 15 ++ packages/next-auth/src/providers/oauth.ts | 2 +- packages/next-auth/src/react/index.tsx | 14 +- packages/next-auth/src/utils/detect-host.ts | 8 + packages/next-auth/src/utils/node.ts | 167 ------------- packages/next-auth/src/utils/parse-url.ts | 9 +- packages/next-auth/src/utils/web.ts | 116 --------- packages/next-auth/tests/assert.test.ts | 10 +- packages/next-auth/tests/email.test.ts | 16 +- .../next-auth/tests/getServerSession.test.ts | 32 +-- packages/next-auth/tests/getURL.test.ts | 138 ----------- packages/next-auth/tests/lib.ts | 68 ++++++ packages/next-auth/tests/middleware.test.ts | 118 +++++---- packages/next-auth/tests/next.test.ts | 179 -------------- packages/next-auth/tests/utils.ts | 224 ------------------ 36 files changed, 453 insertions(+), 1206 deletions(-) create mode 100644 packages/next-auth/src/next/utils.ts create mode 100644 packages/next-auth/src/utils/detect-host.ts delete mode 100644 packages/next-auth/src/utils/node.ts delete mode 100644 packages/next-auth/src/utils/web.ts delete mode 100644 packages/next-auth/tests/getURL.test.ts create mode 100644 packages/next-auth/tests/lib.ts delete mode 100644 packages/next-auth/tests/next.test.ts delete mode 100644 packages/next-auth/tests/utils.ts diff --git a/packages/next-auth/package.json b/packages/next-auth/package.json index 08dc2bb0bd..f3c7c6403e 100644 --- a/packages/next-auth/package.json +++ b/packages/next-auth/package.json @@ -131,4 +131,4 @@ "engines": { "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0" } -} \ No newline at end of file +} diff --git a/packages/next-auth/src/client/_utils.ts b/packages/next-auth/src/client/_utils.ts index 79527a2a25..7ccc07761c 100644 --- a/packages/next-auth/src/client/_utils.ts +++ b/packages/next-auth/src/client/_utils.ts @@ -1,7 +1,7 @@ import type { IncomingMessage } from "http" import type { LoggerInstance, Session } from ".." -export interface AuthClientConfig { +export interface NextAuthClientConfig { baseUrl: string basePath: string baseUrlServer: string @@ -31,7 +31,7 @@ export interface CtxOrReq { */ export async function fetchData( path: string, - __NEXTAUTH: AuthClientConfig, + __NEXTAUTH: NextAuthClientConfig, logger: LoggerInstance, { ctx, req = ctx?.req }: CtxOrReq = {} ): Promise { @@ -50,7 +50,7 @@ export async function fetchData( } } -export function apiBaseUrl(__NEXTAUTH: AuthClientConfig) { +export function apiBaseUrl(__NEXTAUTH: NextAuthClientConfig) { if (typeof window === "undefined") { // Return absolute path when called server side return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}` diff --git a/packages/next-auth/src/core/errors.ts b/packages/next-auth/src/core/errors.ts index 1b9b91e26c..b2eaf0ba09 100644 --- a/packages/next-auth/src/core/errors.ts +++ b/packages/next-auth/src/core/errors.ts @@ -72,16 +72,6 @@ export class InvalidCallbackUrl extends UnknownError { code = "INVALID_CALLBACK_URL_ERROR" } -export class UnknownAction extends UnknownError { - name = "UnknownAction" - code = "UNKNOWN_ACTION_ERROR" -} - -export class UntrustedHost extends UnknownError { - name = "UntrustedHost" - code = "UNTRUST_HOST_ERROR" -} - type Method = (...args: any[]) => Promise export function upperSnake(s: string) { diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 9b7e4c09a0..7d1b39a522 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -1,55 +1,92 @@ import logger, { setLogger } from "../utils/logger" -import { toInternalRequest, toResponse } from "../utils/web" +import { detectHost } from "../utils/detect-host" +import * as routes from "./routes" +import renderPage from "./pages" import { init } from "./init" import { assertConfig } from "./lib/assert" import { SessionStore } from "./lib/cookie" -import renderPage from "./pages" -import * as routes from "./routes" -import { UntrustedHost } from "./errors" +import type { NextAuthAction, NextAuthOptions } from "./types" import type { Cookie } from "./lib/cookie" import type { ErrorType } from "./pages/error" -import type { AuthAction, AuthOptions } from "./types" +import { parse as parseCookie } from "cookie" -/** @internal */ export interface RequestInternal { - url: URL - /** @default "GET" */ - method: string + /** @default "http://localhost:3000" */ + host?: string + method?: string cookies?: Partial> headers?: Record query?: Record body?: Record - action: AuthAction + action: NextAuthAction providerId?: string error?: string } -/** @internal */ -export interface ResponseInternal< +export interface NextAuthHeader { + key: string + value: string +} + +export interface OutgoingResponse< Body extends string | Record | any[] = any > { status?: number - headers?: Record + headers?: NextAuthHeader[] body?: Body redirect?: string cookies?: Cookie[] } -const configErrorMessage = - "There is a problem with the server configuration. Check the server logs for more information." +export interface NextAuthHandlerParams { + req: Request | RequestInternal + options: NextAuthOptions +} + +async function getBody(req: Request): Promise | undefined> { + try { + return await req.json() + } catch {} +} + +// TODO: +async function toInternalRequest( + req: RequestInternal | Request +): Promise { + 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 = Object.fromEntries(url.searchParams) + query.nextauth = nextauth -async function AuthHandlerInternal< + return { + action: nextauth[0] as NextAuthAction, + method: req.method, + headers, + body: await getBody(req), + cookies: parseCookie(req.headers.get("cookie") ?? ""), + providerId: nextauth[1], + error: url.searchParams.get("error") ?? nextauth[1], + host: detectHost(headers["x-forwarded-host"] ?? headers.host), + query, + } + } + return req +} + +export async function NextAuthHandler< Body extends string | Record | any[] ->(params: { - req: RequestInternal - options: AuthOptions - /** REVIEW: Is this the best way to skip parsing the body in Node.js? */ - parsedBody?: any -}): Promise> { - const { options: authOptions, req } = params +>(params: NextAuthHandlerParams): Promise> { + const { options: userOptions, req: incomingRequest } = params + + const req = await toInternalRequest(incomingRequest) - const assertionResult = assertConfig({ options: authOptions, req }) + setLogger(userOptions.logger, userOptions.debug) + + const assertionResult = assertConfig({ options: userOptions, req }) if (Array.isArray(assertionResult)) { assertionResult.forEach(logger.warn) @@ -59,13 +96,14 @@ async function AuthHandlerInternal< 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: { "Content-Type": "application/json" }, - body: { message: configErrorMessage } as any, + headers: [{ key: "Content-Type", value: "application/json" }], + body: { message } as any, } } - const { pages, theme } = authOptions + const { pages, theme } = userOptions const authOnErrorPage = pages?.error && req.query?.callbackUrl?.startsWith(pages.error) @@ -88,13 +126,13 @@ async function AuthHandlerInternal< } } - const { action, providerId, error, method } = req + const { action, providerId, error, method = "GET" } = req const { options, cookies } = await init({ - authOptions, + userOptions, action, providerId, - url: req.url, + host: req.host, callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl, csrfToken: req.body?.csrfToken, cookies: req.cookies, @@ -116,12 +154,11 @@ async function AuthHandlerInternal< case "session": { const session = await routes.session({ options, sessionStore }) if (session.cookies) cookies.push(...session.cookies) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return { ...session, cookies } as any } case "csrf": return { - headers: { "Content-Type": "application/json" }, + headers: [{ key: "Content-Type", value: "application/json" }], body: { csrfToken: options.csrfToken } as any, cookies, } @@ -238,7 +275,7 @@ async function AuthHandlerInternal< } break case "_log": - if (authOptions.logger) { + if (userOptions.logger) { try { const { code, level, ...metadata } = req.body ?? {} logger[level](code, metadata) @@ -257,51 +294,3 @@ async function AuthHandlerInternal< body: `Error: This action with HTTP ${method} is not supported by NextAuth.js` as any, } } - -/** - * The core functionality of `next-auth`. - * It receives a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) - * and returns a standard [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). - */ -export async function AuthHandler( - request: Request, - options: AuthOptions -): Promise { - setLogger(options.logger, options.debug) - - if (!options.trustHost) { - const error = new UntrustedHost( - `Host must be trusted. URL was: ${request.url}` - ) - logger.error(error.code, error) - - return new Response(JSON.stringify({ message: configErrorMessage }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }) - } - - const req = await toInternalRequest(request) - if (req instanceof Error) { - logger.error((req as any).code, req) - return new Response( - `Error: This action with HTTP ${request.method} is not supported.`, - { status: 400 } - ) - } - const internalResponse = await AuthHandlerInternal({ req, options }) - - const response = await toResponse(internalResponse) - - // If the request expects a return URL, send it as JSON - // instead of doing an actual redirect. - const redirect = response.headers.get("Location") - if (request.headers.has("X-Auth-Return-Redirect") && redirect) { - response.headers.delete("Location") - response.headers.set("Content-Type", "application/json") - return new Response(JSON.stringify({ url: redirect }), { - headers: response.headers, - }) - } - return response -} diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index ede030a56e..b5fa4a72e4 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -1,6 +1,7 @@ import { randomBytes, randomUUID } from "crypto" -import { AuthOptions } from ".." +import { NextAuthOptions } from ".." import logger from "../utils/logger" +import parseUrl from "../utils/parse-url" import { adapterErrorHandler, eventsErrorHandler } from "./errors" import parseProviders from "./lib/providers" import { createSecret } from "./lib/utils" @@ -12,11 +13,10 @@ import { createCallbackUrl } from "./lib/callback-url" import { RequestInternal } from "." import type { InternalOptions } from "./types" -import parseUrl from "../utils/parse-url" interface InitParams { - url: URL - authOptions: AuthOptions + host?: string + userOptions: NextAuthOptions providerId?: string action: InternalOptions["action"] /** Callback URL value extracted from the incoming request. */ @@ -30,10 +30,10 @@ interface InitParams { /** Initialize all internal options and cookies. */ export async function init({ - authOptions, + userOptions, providerId, action, - url: reqUrl, + host, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, @@ -42,17 +42,12 @@ export async function init({ options: InternalOptions cookies: cookie.Cookie[] }> { - // TODO: move this to web.ts - const parsed = parseUrl( - reqUrl.origin + - reqUrl.pathname.replace(`/${action}`, "").replace(`/${providerId}`, "") - ) - const url = new URL(parsed.toString()) + const url = parseUrl(host) - const secret = createSecret({ authOptions, url }) + const secret = createSecret({ userOptions, url }) const { providers, provider } = parseProviders({ - providers: authOptions.providers, + providers: userOptions.providers, url, providerId, }) @@ -71,7 +66,7 @@ export async function init({ buttonText: "", }, // Custom options override defaults - ...authOptions, + ...userOptions, // These computed settings can have values in userOptions but we override them // and are request-specific. url, @@ -80,24 +75,24 @@ export async function init({ provider, cookies: { ...cookie.defaultCookies( - authOptions.useSecureCookies ?? url.protocol === "https:" + userOptions.useSecureCookies ?? url.base.startsWith("https://") ), // Allow user cookie options to override any cookie settings above - ...authOptions.cookies, + ...userOptions.cookies, }, secret, providers, // Session options session: { // If no adapter specified, force use of JSON Web Tokens (stateless) - strategy: authOptions.adapter ? "database" : "jwt", + strategy: userOptions.adapter ? "database" : "jwt", maxAge, updateAge: 24 * 60 * 60, generateSessionToken: () => { // Use `randomUUID` if available. (Node 15.6+) return randomUUID?.() ?? randomBytes(32).toString("hex") }, - ...authOptions.session, + ...userOptions.session, }, // JWT options jwt: { @@ -105,13 +100,13 @@ export async function init({ maxAge, // same as session maxAge, encode: jwt.encode, decode: jwt.decode, - ...authOptions.jwt, + ...userOptions.jwt, }, // Event messages - events: eventsErrorHandler(authOptions.events ?? {}, logger), - adapter: adapterErrorHandler(authOptions.adapter, logger), + events: eventsErrorHandler(userOptions.events ?? {}, logger), + adapter: adapterErrorHandler(userOptions.adapter, logger), // Callback functions - callbacks: { ...defaultCallbacks, ...authOptions.callbacks }, + callbacks: { ...defaultCallbacks, ...userOptions.callbacks }, logger, callbackUrl: url.origin, } diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index 589a025d82..d1e5bb3533 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -7,11 +7,12 @@ import { 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" +import type { NextAuthOptions } from "../types" type ConfigError = | MissingAPIRoute @@ -39,15 +40,15 @@ function isValidHttpUrl(url: string, baseUrl: string) { * REVIEW: Make some of these and corresponding docs less Next.js specific? */ export function assertConfig(params: { - options: AuthOptions + options: NextAuthOptions req: RequestInternal }): ConfigError | WarningCode[] { const { options, req } = params - const { url } = req + const warnings: WarningCode[] = [] if (!warned) { - if (!url.origin) warnings.push("NEXTAUTH_URL") + if (!req.host) 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") @@ -69,19 +70,21 @@ export function assertConfig(params: { const callbackUrlParam = req.query?.callbackUrl as string | undefined - if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.origin)) { + const url = parseUrl(req.host) + + if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) { return new InvalidCallbackUrl( `Invalid callback URL. Received: ${callbackUrlParam}` ) } const { callbackUrl: defaultCallbackUrl } = defaultCookies( - options.useSecureCookies ?? url.protocol === "https://" + options.useSecureCookies ?? url.base.startsWith("https://") ) const callbackUrlCookie = req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name] - if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.origin)) { + if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) { return new InvalidCallbackUrl( `Invalid callback URL. Received: ${callbackUrlCookie}` ) diff --git a/packages/next-auth/src/core/lib/providers.ts b/packages/next-auth/src/core/lib/providers.ts index 5f350add7f..423969990f 100644 --- a/packages/next-auth/src/core/lib/providers.ts +++ b/packages/next-auth/src/core/lib/providers.ts @@ -2,10 +2,11 @@ import { merge } from "../../utils/merge" import type { InternalProvider } from "../types" import type { - OAuthConfigInternal, + InternalOAuthConfig, OAuthConfig, Provider, } from "../../providers" +import type { InternalUrl } from "../../utils/parse-url" /** * Adds `signinUrl` and `callbackUrl` to each provider @@ -13,7 +14,7 @@ import type { */ export default function parseProviders(params: { providers: Provider[] - url: URL + url: InternalUrl providerId?: string }): { providers: InternalProvider[] @@ -58,7 +59,7 @@ function normalizeOAuthOptions( if (!oauthOptions) return const normalized = Object.entries(oauthOptions).reduce< - OAuthConfigInternal> + InternalOAuthConfig> >( (acc, [key, value]) => { if ( diff --git a/packages/next-auth/src/core/lib/utils.ts b/packages/next-auth/src/core/lib/utils.ts index c2ed6991c5..9d61a7b7a8 100644 --- a/packages/next-auth/src/core/lib/utils.ts +++ b/packages/next-auth/src/core/lib/utils.ts @@ -1,7 +1,8 @@ import { createHash } from "crypto" -import type { AuthOptions } from "../.." +import type { NextAuthOptions } 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. @@ -27,14 +28,17 @@ export function hashToken(token: string, options: InternalOptions<"email">) { * 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: URL }) { - const { authOptions, url } = params +export function createSecret(params: { + userOptions: NextAuthOptions + url: InternalUrl +}) { + const { userOptions, url } = params return ( - authOptions.secret ?? + userOptions.secret ?? // TODO: Remove falling back to default secret, and error in dev if one isn't provided createHash("sha256") - .update(JSON.stringify({ ...url, ...authOptions })) + .update(JSON.stringify({ ...url, ...userOptions })) .digest("hex") ) } diff --git a/packages/next-auth/src/core/pages/error.tsx b/packages/next-auth/src/core/pages/error.tsx index b2b803b38f..e3f5562e57 100644 --- a/packages/next-auth/src/core/pages/error.tsx +++ b/packages/next-auth/src/core/pages/error.tsx @@ -1,4 +1,5 @@ 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. @@ -11,7 +12,7 @@ export type ErrorType = | "verification" export interface ErrorProps { - url?: URL + url?: InternalUrl theme?: Theme error?: ErrorType } diff --git a/packages/next-auth/src/core/pages/index.ts b/packages/next-auth/src/core/pages/index.ts index 8eed148e73..9812497aa0 100644 --- a/packages/next-auth/src/core/pages/index.ts +++ b/packages/next-auth/src/core/pages/index.ts @@ -6,7 +6,7 @@ import ErrorPage from "./error" import css from "../../css" import type { InternalOptions } from "../types" -import type { RequestInternal, ResponseInternal } from ".." +import type { RequestInternal, OutgoingResponse } from ".." import type { Cookie } from "../lib/cookie" import type { ErrorType } from "./error" @@ -27,11 +27,11 @@ type RenderPageParams = { export default function renderPage(params: RenderPageParams) { const { url, theme, query, cookies } = params - function send({ html, title, status }: any): ResponseInternal { + function send({ html, title, status }: any): OutgoingResponse { return { cookies, status, - headers: { "Content-Type": "text/html" }, + headers: [{ key: "Content-Type", value: "text/html" }], body: `${title}
${renderToString(html)}
`, diff --git a/packages/next-auth/src/core/pages/signout.tsx b/packages/next-auth/src/core/pages/signout.tsx index 3d986a1040..352d825753 100644 --- a/packages/next-auth/src/core/pages/signout.tsx +++ b/packages/next-auth/src/core/pages/signout.tsx @@ -1,7 +1,8 @@ import { Theme } from "../.." +import { InternalUrl } from "../../utils/parse-url" export interface SignoutProps { - url: URL + url: InternalUrl csrfToken: string theme: Theme } diff --git a/packages/next-auth/src/core/routes/callback.ts b/packages/next-auth/src/core/routes/callback.ts index b7008d6453..c42bfa3f9d 100644 --- a/packages/next-auth/src/core/routes/callback.ts +++ b/packages/next-auth/src/core/routes/callback.ts @@ -4,7 +4,7 @@ import { hashToken } from "../lib/utils" import getAdapterUserFromEmail from "../lib/email/getUserFromEmail" import type { InternalOptions } from "../types" -import type { RequestInternal, ResponseInternal } from ".." +import type { RequestInternal, OutgoingResponse } from ".." import type { Cookie, SessionStore } from "../lib/cookie" import type { User } from "../.." import type { AdapterSession } from "../../adapters" @@ -18,7 +18,7 @@ export default async function callback(params: { headers: RequestInternal["headers"] cookies: RequestInternal["cookies"] sessionStore: SessionStore -}): Promise { +}): Promise { const { options, query, body, method, headers, sessionStore } = params const { provider, diff --git a/packages/next-auth/src/core/routes/providers.ts b/packages/next-auth/src/core/routes/providers.ts index 2f6f1b0fab..f946538c5f 100644 --- a/packages/next-auth/src/core/routes/providers.ts +++ b/packages/next-auth/src/core/routes/providers.ts @@ -1,4 +1,4 @@ -import type { ResponseInternal } from ".." +import type { OutgoingResponse } from ".." import type { InternalProvider } from "../types" export interface PublicProvider { @@ -16,9 +16,9 @@ export interface PublicProvider { */ export default function providers( providers: InternalProvider[] -): ResponseInternal> { +): OutgoingResponse> { return { - headers: { "Content-Type": "application/json" }, + headers: [{ key: "Content-Type", value: "application/json" }], body: providers.reduce>( (acc, { id, name, type, signinUrl, callbackUrl }) => { acc[id] = { id, name, type, signinUrl, callbackUrl } diff --git a/packages/next-auth/src/core/routes/session.ts b/packages/next-auth/src/core/routes/session.ts index a7eabc5113..8989bc7138 100644 --- a/packages/next-auth/src/core/routes/session.ts +++ b/packages/next-auth/src/core/routes/session.ts @@ -2,7 +2,7 @@ import { fromDate } from "../lib/utils" import type { Adapter } from "../../adapters" import type { InternalOptions } from "../types" -import type { ResponseInternal } from ".." +import type { OutgoingResponse } from ".." import type { Session } from "../.." import type { SessionStore } from "../lib/cookie" @@ -18,7 +18,7 @@ interface SessionParams { export default async function session( params: SessionParams -): Promise> { +): Promise> { const { options, sessionStore } = params const { adapter, @@ -29,9 +29,9 @@ export default async function session( session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, } = options - const response: ResponseInternal = { + const response: OutgoingResponse = { body: {}, - headers: { "Content-Type": "application/json" }, + headers: [{ key: "Content-Type", value: "application/json" }], cookies: [], } diff --git a/packages/next-auth/src/core/routes/signin.ts b/packages/next-auth/src/core/routes/signin.ts index 201f279101..3b85abc52c 100644 --- a/packages/next-auth/src/core/routes/signin.ts +++ b/packages/next-auth/src/core/routes/signin.ts @@ -1,7 +1,7 @@ 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 { RequestInternal, OutgoingResponse } from ".." import type { InternalOptions } from "../types" import type { Account } from "../.." @@ -10,7 +10,7 @@ export default async function signin(params: { options: InternalOptions<"oauth" | "email"> query: RequestInternal["query"] body: RequestInternal["body"] -}): Promise { +}): Promise { const { options, query, body } = params const { url, callbacks, logger, provider } = options diff --git a/packages/next-auth/src/core/routes/signout.ts b/packages/next-auth/src/core/routes/signout.ts index 5078d53fb5..69648aabcd 100644 --- a/packages/next-auth/src/core/routes/signout.ts +++ b/packages/next-auth/src/core/routes/signout.ts @@ -1,13 +1,13 @@ import type { Adapter } from "../../adapters" import type { InternalOptions } from "../types" -import type { ResponseInternal } from ".." +import type { OutgoingResponse } from ".." import type { SessionStore } from "../lib/cookie" /** Handle requests to /api/auth/signout */ export default async function signout(params: { options: InternalOptions sessionStore: SessionStore -}): Promise { +}): Promise { const { options, sessionStore } = params const { adapter, events, jwt, callbackUrl, logger, session } = options diff --git a/packages/next-auth/src/core/types.ts b/packages/next-auth/src/core/types.ts index 11137b7a2b..8f192d9507 100644 --- a/packages/next-auth/src/core/types.ts +++ b/packages/next-auth/src/core/types.ts @@ -5,7 +5,7 @@ import type { ProviderType, EmailConfig, CredentialsConfig, - OAuthConfigInternal, + InternalOAuthConfig, } from "../providers" import type { TokenSetParameters } from "openid-client" import type { JWT, JWTOptions } from "../jwt" @@ -14,6 +14,8 @@ import type { CookieSerializeOptions } from "cookie" import type { NextApiRequest, NextApiResponse } from "next" +import type { InternalUrl } from "../utils/parse-url" + export type Awaitable = T | PromiseLike export type { LoggerInstance } @@ -23,7 +25,7 @@ export type { LoggerInstance } * * [Documentation](https://next-auth.js.org/configuration/options#options) */ -export interface AuthOptions { +export interface NextAuthOptions { /** * An array of authentication providers for signing in * (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order. @@ -201,16 +203,6 @@ export interface AuthOptions { * [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example) */ cookies?: Partial - /** - * If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers, - * instead of `NEXTAUTH_URL` - * Make sure that reading `x-forwarded-host` on your hosting platform can be trusted. - * - ⚠ **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. - * @default Boolean(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL) - */ - trustHost?: boolean } /** @@ -499,7 +491,7 @@ export interface User extends DefaultUser {} /** @internal */ export type InternalProvider = (T extends "oauth" - ? OAuthConfigInternal + ? InternalOAuthConfig : T extends "email" ? EmailConfig : T extends "credentials" @@ -509,7 +501,7 @@ export type InternalProvider = (T extends "oauth" callbackUrl: string } -export type AuthAction = +export type NextAuthAction = | "providers" | "session" | "csrf" @@ -526,8 +518,12 @@ export interface InternalOptions< WithVerificationToken = TProviderType extends "email" ? true : false > { providers: InternalProvider[] - url: URL - action: AuthAction + /** + * Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel. + * @default "http://localhost:3000/api/auth" + */ + url: InternalUrl + action: NextAuthAction provider: InternalProvider csrfToken?: string csrfTokenVerified?: boolean diff --git a/packages/next-auth/src/index.ts b/packages/next-auth/src/index.ts index 1efe460d51..401838bba1 100644 --- a/packages/next-auth/src/index.ts +++ b/packages/next-auth/src/index.ts @@ -1,4 +1,6 @@ export * from "./core/types" -export type { AuthOptions as NextAuthOptions } from "./core/types" + +export type { RequestInternal, OutgoingResponse } from "./core" + export * from "./next" export { default } from "./next" diff --git a/packages/next-auth/src/jwt/index.ts b/packages/next-auth/src/jwt/index.ts index aec2e0492a..03df8e8a10 100644 --- a/packages/next-auth/src/jwt/index.ts +++ b/packages/next-auth/src/jwt/index.ts @@ -94,7 +94,7 @@ export async function getToken( const authorizationHeader = req.headers instanceof Headers ? req.headers.get("authorization") - : req.headers?.authorization + : req.headers.authorization if (!token && authorizationHeader?.split(" ")[0] === "Bearer") { const urlEncodedToken = authorizationHeader.split(" ")[1] diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 7697892fad..4e4119c610 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -1,74 +1,85 @@ -import { AuthHandler } from "../core" -import { getBody, getURL, setHeaders } from "../utils/node" +import { NextAuthHandler } from "../core" +import { detectHost } from "../utils/detect-host" +import { setCookie } from "./utils" import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse, } from "next" -import type { AuthOptions, Session } from ".." +import type { NextAuthOptions, Session } from ".." import type { CallbacksOptions, + NextAuthAction, NextAuthRequest, NextAuthResponse, } from "../core/types" -async function NextAuthHandler( +async function NextAuthNextHandler( req: NextApiRequest, res: NextApiResponse, - options: AuthOptions + options: NextAuthOptions ) { - const headers = new Headers(req.headers as any) - const url = getURL(req.url, headers) - if (url instanceof Error) { - if (process.env.NODE_ENV !== "production") throw url - const errorLogger = options.logger?.error ?? console.error - errorLogger("INVALID_URL", url) - res.status(400) - return res.json({ - message: - "There is a problem with the server configuration. Check the server logs for more information.", - }) - } - - const request = new Request(url, { - headers, - method: req.method, - ...getBody(req), + const { nextauth, ...query } = req.query + + options.secret = + options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET + + const handler = await NextAuthHandler({ + req: { + host: detectHost(req.headers["x-forwarded-host"]), + body: req.body, + query, + cookies: req.cookies, + headers: req.headers, + method: req.method, + action: nextauth?.[0] as NextAuthAction, + providerId: nextauth?.[1], + error: (req.query.error as string | undefined) ?? nextauth?.[1], + }, + options, }) - options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET - options.trustHost ??= !!( - process.env.NEXTAUTH_URL ?? - process.env.AUTH_TRUST_HOST ?? - process.env.VERCEL ?? - process.env.NODE_ENV !== "production" - ) + res.status(handler.status ?? 200) - const response = await AuthHandler(request, options) - res.status(response.status) - setHeaders(response.headers, res) + handler.cookies?.forEach((cookie) => setCookie(res, cookie)) - return res.send(await response.text()) + handler.headers?.forEach((h) => res.setHeader(h.key, h.value)) + + if (handler.redirect) { + // If the request expects a return URL, send it as JSON + // instead of doing an actual redirect. + if (req.body?.json !== "true") { + // Could chain. .end() when lowest target is Node 14 + // https://github.com/nodejs/node/issues/33148 + res.status(302).setHeader("Location", handler.redirect) + return res.end() + } + return res.json({ url: handler.redirect }) + } + + return res.send(handler.body) } -function NextAuth(options: AuthOptions): any +function NextAuth(options: NextAuthOptions): any function NextAuth( req: NextApiRequest, res: NextApiResponse, - options: AuthOptions + options: NextAuthOptions ): any /** The main entry point to next-auth */ function NextAuth( - ...args: [AuthOptions] | [NextApiRequest, NextApiResponse, AuthOptions] + ...args: + | [NextAuthOptions] + | [NextApiRequest, NextApiResponse, NextAuthOptions] ) { if (args.length === 1) { return async (req: NextAuthRequest, res: NextAuthResponse) => - await NextAuthHandler(req, res, args[0]) + await NextAuthNextHandler(req, res, args[0]) } - return NextAuthHandler(args[0], args[1], args[2]) + return NextAuthNextHandler(args[0], args[1], args[2]) } export default NextAuth @@ -76,8 +87,8 @@ export default NextAuth let experimentalWarningShown = false let experimentalRSCWarningShown = false -type GetServerSessionOptions = Partial> & { - callbacks?: Omit & { +type GetServerSessionOptions = Partial> & { + callbacks?: Omit & { session?: (...args: Parameters) => any } } @@ -119,7 +130,7 @@ export async function unstable_getServerSession< experimentalRSCWarningShown = true } - let req, res, options: AuthOptions + let req, res, options: NextAuthOptions if (isRSC) { options = Object.assign({}, args[0], { providers: [] }) @@ -140,39 +151,41 @@ export async function unstable_getServerSession< options = Object.assign({}, args[2], { providers: [] }) } - const url = getURL("/api/auth/session", new Headers(req.headers)) - if (url instanceof Error) { - if (process.env.NODE_ENV !== "production") throw url - const errorLogger = options.logger?.error ?? console.error - errorLogger("INVALID_URL", url) - res.status(400) - return res.json({ - message: - "There is a problem with the server configuration. Check the server logs for more information.", - }) - } - - const request = new Request(url, { headers: new Headers(req.headers) }) - - options.secret ??= process.env.NEXTAUTH_SECRET - options.trustHost = true - const response = await AuthHandler(request, options) - - const { status = 200, headers } = response + options.secret = options.secret ?? process.env.NEXTAUTH_SECRET + + const session = await NextAuthHandler({ + options, + req: { + host: detectHost(req.headers["x-forwarded-host"]), + action: "session", + method: "GET", + cookies: req.cookies, + headers: req.headers, + }, + }) - setHeaders(headers, res) + const { body, cookies, status = 200 } = session - // This would otherwise break rendering - // with `getServerSideProps` that needs to always return HTML - res.removeHeader?.("Content-Type") + cookies?.forEach((cookie) => setCookie(res, cookie)) - const data = await response.json() + if (body && typeof body !== "string" && Object.keys(body).length) { + if (status === 200) { + // @ts-expect-error + if (isRSC) delete body.expires + return body as R + } + throw new Error((body as any).message) + } - if (!data || !Object.keys(data).length) return null + return null +} - if (status === 200) { - if (isRSC) delete data.expires - return data as R +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace NodeJS { + interface ProcessEnv { + NEXTAUTH_URL?: string + VERCEL?: "1" + } } - throw new Error(data.message) } diff --git a/packages/next-auth/src/next/middleware.ts b/packages/next-auth/src/next/middleware.ts index f9dfe1c9c3..0b958149a3 100644 --- a/packages/next-auth/src/next/middleware.ts +++ b/packages/next-auth/src/next/middleware.ts @@ -1,5 +1,5 @@ import type { NextMiddleware, NextFetchEvent } from "next/server" -import type { Awaitable, CookieOption, AuthOptions } from ".." +import type { Awaitable, CookieOption, NextAuthOptions } from ".." import type { JWT, JWTOptions } from "../jwt" import { NextResponse, NextRequest } from "next/server" @@ -7,17 +7,6 @@ import { NextResponse, NextRequest } from "next/server" import { getToken } from "../jwt" import parseUrl from "../utils/parse-url" -// // TODO: Remove -/** Extract the host from the environment */ -export function detectHost( - trusted: boolean, - forwardedValue: string | null, - defaultValue: string | false -): string | undefined { - if (trusted && forwardedValue) return forwardedValue - return defaultValue || undefined -} - type AuthorizedCallback = (params: { token: JWT | null req: NextRequest @@ -31,7 +20,7 @@ export interface NextAuthMiddlewareOptions { * --- * [Documentation](https://next-auth.js.org/configuration/pages) */ - pages?: AuthOptions["pages"] + pages?: NextAuthOptions["pages"] /** * You can override the default cookie names and options for any of the cookies @@ -49,7 +38,7 @@ export interface NextAuthMiddlewareOptions { */ cookies?: Partial< Record< - keyof Pick, + keyof Pick, Omit > > @@ -67,7 +56,7 @@ export interface NextAuthMiddlewareOptions { * Callback that receives the user's JWT payload * and returns `true` to allow the user to continue. * - * This is similar to the `signIn` callback in `AuthOptions`. + * This is similar to the `signIn` callback in `NextAuthOptions`. * * If it returns `false`, the user is redirected to the sign-in page instead * @@ -100,43 +89,23 @@ export interface NextAuthMiddlewareOptions { * The same `secret` used in the `NextAuth` configuration. * Defaults to the `NEXTAUTH_SECRET` environment variable. */ - secret?: AuthOptions["secret"] - /** - * If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers, - * instead of `NEXTAUTH_URL` - * Make sure that reading `x-forwarded-host` on your hosting platform can be trusted. - * - ⚠ **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. - * @default Boolean(process.env.VERCEL ?? process.env.AUTH_TRUST_HOST) - */ - trustHost?: AuthOptions["trustHost"] + secret?: string } +// TODO: `NextMiddleware` should allow returning `void` +// Simplify when https://github.com/vercel/next.js/pull/38625 is merged. +type NextMiddlewareResult = ReturnType | void // eslint-disable-line @typescript-eslint/no-invalid-void-type + async function handleMiddleware( req: NextRequest, - options: NextAuthMiddlewareOptions | undefined = {}, - onSuccess?: (token: JWT | null) => ReturnType + options: NextAuthMiddlewareOptions | undefined, + onSuccess?: (token: JWT | null) => Promise ) { const { pathname, search, origin, basePath } = req.nextUrl const signInPage = options?.pages?.signIn ?? "/api/auth/signin" const errorPage = options?.pages?.error ?? "/api/auth/error" - - options.trustHost ??= !!( - process.env.NEXTAUTH_URL ?? - process.env.VERCEL ?? - process.env.AUTH_TRUST_HOST - ) - - const host = detectHost( - options.trustHost, - req.headers?.get("x-forwarded-host"), - process.env.NEXTAUTH_URL ?? - (process.env.NODE_ENV !== "production" && "http://localhost:3000") - ) - const authPath = parseUrl(host).path - + const authPath = parseUrl(process.env.NEXTAUTH_URL).path const publicPaths = ["/_next", "/favicon.ico"] // Avoid infinite redirects/invalid response @@ -149,8 +118,8 @@ async function handleMiddleware( return } - options.secret ??= process.env.NEXTAUTH_SECRET - if (!options.secret) { + const secret = options?.secret ?? process.env.NEXTAUTH_SECRET + if (!secret) { console.error( `[next-auth][error][NO_SECRET]`, `\nhttps://next-auth.js.org/errors#no_secret` @@ -164,9 +133,9 @@ async function handleMiddleware( const token = await getToken({ req, - decode: options.jwt?.decode, + decode: options?.jwt?.decode, cookieName: options?.cookies?.sessionToken?.name, - secret: options.secret, + secret, }) const isAuthorized = @@ -177,10 +146,7 @@ async function handleMiddleware( // the user is not logged in, redirect to the sign-in page const signInUrl = new URL(`${basePath}${signInPage}`, origin) - signInUrl.searchParams.append( - "callbackUrl", - `${basePath}${pathname}${search}` - ) + signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`) return NextResponse.redirect(signInUrl) } @@ -191,7 +157,7 @@ export interface NextRequestWithAuth extends NextRequest { export type NextMiddlewareWithAuth = ( request: NextRequestWithAuth, event: NextFetchEvent -) => ReturnType +) => NextMiddlewareResult | Promise export type WithAuthArgs = | [NextRequestWithAuth] diff --git a/packages/next-auth/src/next/utils.ts b/packages/next-auth/src/next/utils.ts new file mode 100644 index 0000000000..6e5769a10f --- /dev/null +++ b/packages/next-auth/src/next/utils.ts @@ -0,0 +1,15 @@ +import { serialize } from "cookie" +import { Cookie } from "../core/lib/cookie" + +export function setCookie(res, cookie: Cookie) { + // Preserve any existing cookies that have already been set in the same session + let setCookieHeader = res.getHeader("Set-Cookie") ?? [] + // If not an array (i.e. a string with a single cookie) convert it into an array + if (!Array.isArray(setCookieHeader)) { + setCookieHeader = [setCookieHeader] + } + const { name, value, options } = cookie + const cookieHeader = serialize(name, value, options) + setCookieHeader.push(cookieHeader) + res.setHeader("Set-Cookie", setCookieHeader) +} diff --git a/packages/next-auth/src/providers/oauth.ts b/packages/next-auth/src/providers/oauth.ts index c476279b12..c0ae9fe3b0 100644 --- a/packages/next-auth/src/providers/oauth.ts +++ b/packages/next-auth/src/providers/oauth.ts @@ -160,7 +160,7 @@ export interface OAuthConfig

extends CommonProviderOptions, PartialIssuer { } /** @internal */ -export interface OAuthConfigInternal

+export interface InternalOAuthConfig

extends Omit, "authorization" | "token" | "userinfo"> { authorization?: AuthorizationEndpointHandler token?: TokenEndpointHandler diff --git a/packages/next-auth/src/react/index.tsx b/packages/next-auth/src/react/index.tsx index e40827a863..07f1f199c9 100644 --- a/packages/next-auth/src/react/index.tsx +++ b/packages/next-auth/src/react/index.tsx @@ -18,7 +18,7 @@ import { apiBaseUrl, fetchData, now, - AuthClientConfig, + NextAuthClientConfig, } from "../client/_utils" import type { @@ -46,7 +46,7 @@ export * from "./types" // relative URLs are valid in that context and so defaults to empty. // 2. When invoked server side the value is picked up from an environment // variable and defaults to 'http://localhost:3000'. -const __NEXTAUTH: AuthClientConfig = { +const __NEXTAUTH: NextAuthClientConfig = { baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin, basePath: parseUrl(process.env.NEXTAUTH_URL).path, baseUrlServer: parseUrl( @@ -124,8 +124,7 @@ export function useSession(options?: UseSessionOptions) { React.useEffect(() => { if (requiredAndNotLoading) { - const baseUrl = apiBaseUrl(__NEXTAUTH) - const url = `${baseUrl}/signin?${new URLSearchParams({ + const url = `/api/auth/signin?${new URLSearchParams({ error: "SessionRequired", callbackUrl: window.location.href, })}` @@ -242,13 +241,13 @@ export async function signIn< method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", - "X-Auth-Return-Redirect": "1", }, // @ts-expect-error body: new URLSearchParams({ ...options, csrfToken: await getCsrfToken(), callbackUrl, + json: true, }), }) @@ -292,11 +291,12 @@ export async function signOut( method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", - "X-Auth-Return-Redirect": "1", }, + // @ts-expect-error body: new URLSearchParams({ - csrfToken: (await getCsrfToken()) ?? "", + csrfToken: await getCsrfToken(), callbackUrl, + json: true, }), } const res = await fetch(`${baseUrl}/signout`, fetchOptions) diff --git a/packages/next-auth/src/utils/detect-host.ts b/packages/next-auth/src/utils/detect-host.ts new file mode 100644 index 0000000000..8c2d346492 --- /dev/null +++ b/packages/next-auth/src/utils/detect-host.ts @@ -0,0 +1,8 @@ +/** Extract the host from the environment */ +export function detectHost(forwardedHost: any) { + // If we detect a Vercel environment, we can trust the host + if (process.env.VERCEL ?? process.env.AUTH_TRUST_HOST) + return forwardedHost + // If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000" + return process.env.NEXTAUTH_URL +} diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts deleted file mode 100644 index 608af63cca..0000000000 --- a/packages/next-auth/src/utils/node.ts +++ /dev/null @@ -1,167 +0,0 @@ -import type { IncomingMessage, ServerResponse } from "http" -import type { GetServerSidePropsContext, NextApiRequest } from "next" - -export function setCookie(res, value: string) { - // Preserve any existing cookies that have already been set in the same session - let setCookieHeader = res.getHeader("Set-Cookie") ?? [] - // If not an array (i.e. a string with a single cookie) convert it into an array - if (!Array.isArray(setCookieHeader)) { - setCookieHeader = [setCookieHeader] - } - setCookieHeader.push(value) - res.setHeader("Set-Cookie", setCookieHeader) -} - -export function getBody( - req: IncomingMessage | NextApiRequest | GetServerSidePropsContext["req"] -) { - if (!("body" in req) || !req.body || req.method !== "POST") { - return - } - - if (req.body instanceof ReadableStream) { - return { body: req.body } - } - return { body: JSON.stringify(req.body) } -} - -/** - * Extract the full request URL from the environment. - * NOTE: It does not verify if the host should be trusted. - */ -export function getURL(url: string | undefined, headers: Headers): URL | Error { - try { - if (!url) throw new Error("Missing url") - if (process.env.NEXTAUTH_URL) { - const base = new URL(process.env.NEXTAUTH_URL) - if (!["http:", "https:"].includes(base.protocol)) { - throw new Error("Invalid protocol") - } - const hasCustomPath = base.pathname !== "/" - - if (hasCustomPath) { - const apiAuthRe = /\/api\/auth\/?$/ - const basePathname = base.pathname.match(apiAuthRe) - ? base.pathname.replace(apiAuthRe, "") - : base.pathname - return new URL(basePathname.replace(/\/$/, "") + url, base.origin) - } - return new URL(url, base) - } - const proto = - headers.get("x-forwarded-proto") ?? - (process.env.NODE_ENV !== "production" ? "http" : "https") - const host = headers.get("x-forwarded-host") ?? headers.get("host") - if (!["http", "https"].includes(proto)) throw new Error("Invalid protocol") - const origin = `${proto}://${host}` - if (!host) throw new Error("Missing host") - return new URL(url, origin) - } catch (error) { - return error as Error - } -} - -/** - * Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas - * that are within a single set-cookie field-value, such as in the Expires portion. - * This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 - * Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 - * Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 - * Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation - * @source https://github.com/nfriedly/set-cookie-parser/blob/3eab8b7d5d12c8ed87832532861c1a35520cf5b3/lib/set-cookie.js#L144 - */ -function getSetCookies(cookiesString: string) { - if (typeof cookiesString !== "string") { - return [] - } - - const cookiesStrings: string[] = [] - let pos = 0 - let start - let ch - let lastComma: number - let nextStart - let cookiesSeparatorFound - - function skipWhitespace() { - while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { - pos += 1 - } - return pos < cookiesString.length - } - - function notSpecialChar() { - ch = cookiesString.charAt(pos) - - return ch !== "=" && ch !== ";" && ch !== "," - } - - while (pos < cookiesString.length) { - start = pos - cookiesSeparatorFound = false - - while (skipWhitespace()) { - ch = cookiesString.charAt(pos) - if (ch === ",") { - // ',' is a cookie separator if we have later first '=', not ';' or ',' - lastComma = pos - pos += 1 - - skipWhitespace() - nextStart = pos - - while (pos < cookiesString.length && notSpecialChar()) { - pos += 1 - } - - // currently special character - if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { - // we found cookies separator - cookiesSeparatorFound = true - // pos is inside the next cookie, so back up and return it. - pos = nextStart - cookiesStrings.push(cookiesString.substring(start, lastComma)) - start = pos - } else { - // in param ',' or param separator ';', - // we continue from that comma - pos = lastComma + 1 - } - } else { - pos += 1 - } - } - - if (!cookiesSeparatorFound || pos >= cookiesString.length) { - cookiesStrings.push(cookiesString.substring(start, cookiesString.length)) - } - } - - return cookiesStrings -} - -export function setHeaders(headers: Headers, res: ServerResponse) { - for (const [key, val] of headers.entries()) { - let value: string | string[] = val - // See: https://github.com/whatwg/fetch/issues/973 - if (key === "set-cookie") { - const cookies = getSetCookies(value) - let original = res.getHeader("set-cookie") as string[] | string - original = Array.isArray(original) ? original : [original] - value = original.concat(cookies).filter(Boolean) - } - res.setHeader(key, value) - } -} - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace NodeJS { - interface ProcessEnv { - AUTH_TRUST_HOST?: string - NEXTAUTH_URL?: string - NEXTAUTH_SECRET?: string - VERCEL?: "1" - } - } -} diff --git a/packages/next-auth/src/utils/parse-url.ts b/packages/next-auth/src/utils/parse-url.ts index 49add525b3..6494c097d6 100644 --- a/packages/next-auth/src/utils/parse-url.ts +++ b/packages/next-auth/src/utils/parse-url.ts @@ -11,14 +11,11 @@ export interface InternalUrl { toString: () => string } -/** - * TODO: Can we remove this? - * Returns an `URL` like object to make requests/redirects from server-side - */ -export default function parseUrl(url?: string | URL): InternalUrl { +/** Returns an `URL` like object to make requests/redirects from server-side */ +export default function parseUrl(url?: string): InternalUrl { const defaultUrl = new URL("http://localhost:3000/api/auth") - if (url && !url.toString().startsWith("http")) { + if (url && !url.startsWith("http")) { url = `https://${url}` } diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts deleted file mode 100644 index 14db7c520c..0000000000 --- a/packages/next-auth/src/utils/web.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { serialize, parse as parseCookie } from "cookie" -import { UnknownAction } from "../core/errors" -import type { ResponseInternal, RequestInternal } from "../core" -import type { AuthAction } from "../core/types" - -const decoder = new TextDecoder() - -async function streamToString(stream): Promise { - const chunks: Uint8Array[] = [] - return await new Promise((resolve, reject) => { - stream.on("data", (chunk) => chunks.push(Buffer.from(chunk))) - stream.on("error", (err) => reject(err)) - stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))) - }) -} - -async function readJSONBody( - body: ReadableStream | Buffer -): Promise | undefined> { - try { - if ("getReader" in body) { - const reader = body.getReader() - const bytes: number[] = [] - while (true) { - const { value, done } = await reader.read() - if (done) break - bytes.push(...value) - } - const b = new Uint8Array(bytes) - return JSON.parse(decoder.decode(b)) - } - - // node-fetch - - if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) { - return JSON.parse(body.toString("utf8")) - } - - return JSON.parse(await streamToString(body)) - } catch (e) { - console.error(e) - } -} - -// prettier-ignore -const actions: AuthAction[] = [ "providers", "session", "csrf", "signin", "signout", "callback", "verify-request", "error", "_log" ] - -export async function toInternalRequest( - req: Request -): Promise { - try { - // TODO: url.toString() should not include action and providerId - // see init.ts - const url = new URL(req.url.replace(/\/$/, "")) - const { pathname } = url - - const action = actions.find((a) => pathname.includes(a)) - if (!action) { - throw new UnknownAction("Cannot detect action.") - } - - const providerIdOrAction = pathname.split("/").pop() - let providerId - if ( - providerIdOrAction && - !action.includes(providerIdOrAction) && - ["signin", "callback"].includes(action) - ) { - providerId = providerIdOrAction - } - - return { - url, - action, - providerId, - method: req.method ?? "GET", - headers: Object.fromEntries(req.headers), - body: req.body ? await readJSONBody(req.body) : undefined, - cookies: parseCookie(req.headers.get("cookie") ?? "") ?? {}, - error: url.searchParams.get("error") ?? undefined, - query: Object.fromEntries(url.searchParams), - } - } catch (error) { - return error - } -} - -export function toResponse(res: ResponseInternal): Response { - const headers = new Headers(res.headers) - - res.cookies?.forEach((cookie) => { - const { name, value, options } = cookie - const cookieHeader = serialize(name, value, options) - if (headers.has("Set-Cookie")) { - headers.append("Set-Cookie", cookieHeader) - } else { - headers.set("Set-Cookie", cookieHeader) - } - }) - - const body = - headers.get("content-type") === "application/json" - ? JSON.stringify(res.body) - : res.body - - const response = new Response(body, { - headers, - status: res.redirect ? 302 : res.status ?? 200, - }) - - if (res.redirect) { - response.headers.set("Location", res.redirect) - } - - return response -} diff --git a/packages/next-auth/tests/assert.test.ts b/packages/next-auth/tests/assert.test.ts index 3b8570c27b..794992e9c0 100644 --- a/packages/next-auth/tests/assert.test.ts +++ b/packages/next-auth/tests/assert.test.ts @@ -4,12 +4,12 @@ import { MissingAdapterMethods, MissingSecret, } from "../src/core/errors" -import { handler } from "./utils" +import { handler } from "./lib" import EmailProvider from "../src/providers/email" it("Show error page if secret is not defined", async () => { const { res, log } = await handler( - { providers: [], secret: undefined, trustHost: true }, + { providers: [], secret: undefined }, { prod: true } ) @@ -28,7 +28,6 @@ it("Show error page if adapter is missing functions when using with email", asyn adapter: missingFunctionAdapter, providers: [EmailProvider({ sendVerificationRequest })], secret: "secret", - trustHost: true, }, { prod: true } ) @@ -49,7 +48,6 @@ it("Show error page if adapter is not configured when using with email", async ( { providers: [EmailProvider({ sendVerificationRequest })], secret: "secret", - trustHost: true, }, { prod: true } ) @@ -66,7 +64,7 @@ it("Show error page if adapter is not configured when using with email", async ( it("Should show configuration error page on invalid `callbackUrl`", async () => { const { res, log } = await handler( - { providers: [], trustHost: true }, + { providers: [] }, { prod: true, params: { callbackUrl: "invalid-callback" } } ) @@ -82,7 +80,7 @@ it("Should show configuration error page on invalid `callbackUrl`", async () => it("Allow relative `callbackUrl`", async () => { const { res, log } = await handler( - { providers: [], trustHost: true }, + { providers: [] }, { prod: true, params: { callbackUrl: "/callback" } } ) diff --git a/packages/next-auth/tests/email.test.ts b/packages/next-auth/tests/email.test.ts index 55d5590cc9..6c7e4a2dd9 100644 --- a/packages/next-auth/tests/email.test.ts +++ b/packages/next-auth/tests/email.test.ts @@ -1,9 +1,8 @@ -import { createCSRF, handler, mockAdapter } from "./utils" +import { createCSRF, handler, mockAdapter } from "./lib" import EmailProvider from "../src/providers/email" it("Send e-mail to the only address correctly", async () => { const { secret, csrf } = await createCSRF() - const sendVerificationRequest = jest.fn() const signIn = jest.fn(() => true) @@ -14,13 +13,12 @@ it("Send e-mail to the only address correctly", async () => { providers: [EmailProvider({ sendVerificationRequest })], callbacks: { signIn }, secret, - trustHost: true, }, { path: "signin/email", requestInit: { method: "POST", - headers: { cookie: csrf.cookie, "content-type": "application/json" }, + headers: { cookie: csrf.cookie }, body: JSON.stringify({ email: email, csrfToken: csrf.value }), }, } @@ -55,13 +53,12 @@ it("Send e-mail to first address only", async () => { providers: [EmailProvider({ sendVerificationRequest })], callbacks: { signIn }, secret, - trustHost: true, }, { path: "signin/email", requestInit: { method: "POST", - headers: { cookie: csrf.cookie, "content-type": "application/json" }, + headers: { cookie: csrf.cookie }, body: JSON.stringify({ email: email, csrfToken: csrf.value }), }, } @@ -96,13 +93,12 @@ it("Send e-mail to address with first domain", async () => { providers: [EmailProvider({ sendVerificationRequest })], callbacks: { signIn }, secret, - trustHost: true, }, { path: "signin/email", requestInit: { method: "POST", - headers: { cookie: csrf.cookie, "content-type": "application/json" }, + headers: { cookie: csrf.cookie }, body: JSON.stringify({ email: email, csrfToken: csrf.value }), }, } @@ -143,13 +139,12 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => { }), ], secret, - trustHost: true, }, { path: "signin/email", requestInit: { method: "POST", - headers: { cookie: csrf.cookie, "content-type": "application/json" }, + headers: { cookie: csrf.cookie }, body: JSON.stringify({ email: "email@email.com,email@email2.com", csrfToken: csrf.value, @@ -161,6 +156,7 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => { expect(signIn).toBeCalledTimes(0) expect(sendVerificationRequest).toBeCalledTimes(0) + // @ts-expect-error expect(log.error.mock.calls[0]).toEqual([ "SIGNIN_EMAIL_ERROR", { error, providerId: "email" }, diff --git a/packages/next-auth/tests/getServerSession.test.ts b/packages/next-auth/tests/getServerSession.test.ts index e169c5ee99..64c30cdc45 100644 --- a/packages/next-auth/tests/getServerSession.test.ts +++ b/packages/next-auth/tests/getServerSession.test.ts @@ -1,7 +1,7 @@ import * as core from "../src/core" import { MissingSecret } from "../src/core/errors" import { unstable_getServerSession } from "../src/next" -import { mockLogger } from "./utils" +import { mockLogger } from "./lib" const originalWarn = console.warn let logger = mockLogger() @@ -83,9 +83,9 @@ describe("Return correct data", () => { }) it("Should return null if there is no session", async () => { - const spy = jest.spyOn(core, "AuthHandler") - // @ts-expect-error [Response.json](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) - spy.mockReturnValue(Promise.resolve(Response.json(null))) + const spy = jest.spyOn(core, "NextAuthHandler") + // @ts-expect-error + spy.mockReturnValue({ body: {} }) const session = await unstable_getServerSession(req, res, { providers: [], @@ -97,19 +97,21 @@ describe("Return correct data", () => { }) it("Should return the session if one is found", async () => { - const mockedBody = { - user: { - name: "John Doe", - email: "test@example.com", - image: "", - id: "1234", + const mockedResponse = { + body: { + user: { + name: "John Doe", + email: "test@example.com", + image: "", + id: "1234", + }, + expires: "", }, - expires: "", } - const spy = jest.spyOn(core, "AuthHandler") - // @ts-expect-error [Response.json](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) - spy.mockReturnValue(Promise.resolve(Response.json(mockedBody))) + const spy = jest.spyOn(core, "NextAuthHandler") + // @ts-expect-error + spy.mockReturnValue(mockedResponse) const session = await unstable_getServerSession(req, res, { providers: [], @@ -117,6 +119,6 @@ describe("Return correct data", () => { secret: "secret", }) - expect(session).toEqual(mockedBody) + expect(session).toEqual(mockedResponse.body) }) }) diff --git a/packages/next-auth/tests/getURL.test.ts b/packages/next-auth/tests/getURL.test.ts deleted file mode 100644 index 61f24f4f3e..0000000000 --- a/packages/next-auth/tests/getURL.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { getURL as getURLOriginal } from "../src/utils/node" - -it("Should return error when missing url", () => { - expect(getURL(undefined, {})).toEqual(new Error("Missing url")) -}) - -it("Should return error when missing host", () => { - expect(getURL("/", {})).toEqual(new Error("Missing host")) -}) - -it("Should return error when invalid protocol", () => { - expect( - getURL("/", { host: "localhost", "x-forwarded-proto": "file" }) - ).toEqual(new Error("Invalid protocol")) -}) - -it("Should return error when invalid host", () => { - expect(getURL("/", { host: "/" })).toEqual( - new TypeError("Invalid base URL: http:///") - ) -}) - -it("Should read host headers", () => { - expect(getURL("/api/auth/session", { host: "localhost" })).toBeURL( - "http://localhost/api/auth/session" - ) - - expect( - getURL("/custom/api/auth/session", { "x-forwarded-host": "localhost:3000" }) - ).toBeURL("http://localhost:3000/custom/api/auth/session") - - // Prefer x-forwarded-host over host - expect( - getURL("/", { host: "localhost", "x-forwarded-host": "localhost:3000" }) - ).toBeURL("http://localhost:3000/") -}) - -it("Should read protocol headers", () => { - expect( - getURL("/", { host: "localhost", "x-forwarded-proto": "http" }) - ).toBeURL("http://localhost/") -}) - -describe("process.env.NEXTAUTH_URL", () => { - afterEach(() => delete process.env.NEXTAUTH_URL) - - it("Should prefer over headers if present", () => { - process.env.NEXTAUTH_URL = "http://localhost:3000" - expect(getURL("/api/auth/session", { host: "localhost" })).toBeURL( - "http://localhost:3000/api/auth/session" - ) - }) - - it("catch errors", () => { - process.env.NEXTAUTH_URL = "invald-url" - expect(getURL("/api/auth/session", {})).toEqual( - new TypeError("Invalid URL: invald-url") - ) - - process.env.NEXTAUTH_URL = "file://localhost" - expect(getURL("/api/auth/session", {})).toEqual( - new TypeError("Invalid protocol") - ) - }) - - it("Supports custom base path", () => { - process.env.NEXTAUTH_URL = "http://localhost:3000/custom/api/auth" - expect(getURL("/api/auth/session", {})).toBeURL( - "http://localhost:3000/custom/api/auth/session" - ) - - // With trailing slash - process.env.NEXTAUTH_URL = "http://localhost:3000/custom/api/auth/" - expect(getURL("/api/auth/session", {})).toBeURL( - "http://localhost:3000/custom/api/auth/session" - ) - - // Multiple custom segments - process.env.NEXTAUTH_URL = "http://localhost:3000/custom/path/api/auth" - expect(getURL("/api/auth/session", {})).toBeURL( - "http://localhost:3000/custom/path/api/auth/session" - ) - - process.env.NEXTAUTH_URL = "http://localhost:3000/custom/path/api/auth/" - expect(getURL("/api/auth/session", {})).toBeURL( - "http://localhost:3000/custom/path/api/auth/session" - ) - - // No /api/auth - process.env.NEXTAUTH_URL = "http://localhost:3000/custom/nextauth" - expect(getURL("/session", {})).toBeURL( - "http://localhost:3000/custom/nextauth/session" - ) - - // No /api/auth, with trailing slash - process.env.NEXTAUTH_URL = "http://localhost:3000/custom/nextauth/" - expect(getURL("/session", {})).toBeURL( - "http://localhost:3000/custom/nextauth/session" - ) - }) -}) - -// Utils - -function getURL( - url: Parameters[0], - headers: HeadersInit -) { - return getURLOriginal(url, new Headers(headers)) -} - -expect.extend({ - toBeURL(rec, exp) { - const r = rec.toString() - const e = exp.toString() - const printR = this.utils.printReceived - const printE = this.utils.printExpected - if (r === e) { - return { - message: () => `expected ${printE(e)} not to be ${printR(r)}`, - pass: true, - } - } - return { - message: () => `expected ${printE(e)}, got ${printR(r)}`, - pass: false, - } - }, -}) - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - toBeURL: (expected: string) => R - } - } -} diff --git a/packages/next-auth/tests/lib.ts b/packages/next-auth/tests/lib.ts new file mode 100644 index 0000000000..7d4a977390 --- /dev/null +++ b/packages/next-auth/tests/lib.ts @@ -0,0 +1,68 @@ +import { createHash } from "crypto" +import { NextAuthHandler } from "../src/core" +import type { LoggerInstance, NextAuthOptions } from "../src" +import type { Adapter } from "../src/adapters" + +export const mockLogger: () => LoggerInstance = () => ({ + error: jest.fn(() => {}), + warn: jest.fn(() => {}), + debug: jest.fn(() => {}), +}) + +interface HandlerOptions { + prod?: boolean + path?: string + params?: URLSearchParams | Record + requestInit?: RequestInit +} + +export async function handler( + options: NextAuthOptions, + { prod, path, params, requestInit }: HandlerOptions +) { + // @ts-expect-error + if (prod) process.env.NODE_ENV = "production" + + const url = new URL( + `http://localhost/api/auth/${path ?? "signin"}?${new URLSearchParams( + params ?? {} + )}` + ) + const req = new Request(url, { headers: { host: "" }, ...requestInit }) + const logger = mockLogger() + const response = await NextAuthHandler({ + req, + options: { secret: "secret", ...options, logger }, + }) + // @ts-expect-error + if (prod) process.env.NODE_ENV = "test" + + return { + res: { + ...response, + html: + response.headers?.[0].value === "text/html" ? response.body : undefined, + }, + log: logger, + } +} + +export function createCSRF() { + const secret = "secret" + const value = "csrf" + const token = createHash("sha256").update(`${value}${secret}`).digest("hex") + + return { + secret, + csrf: { value, token, cookie: `next-auth.csrf-token=${value}|${token}` }, + } +} + +export function mockAdapter(): Adapter { + const adapter: Adapter = { + createVerificationToken: jest.fn(() => {}), + useVerificationToken: jest.fn(() => {}), + getUserByEmail: jest.fn(() => {}), + } + return adapter +} diff --git a/packages/next-auth/tests/middleware.test.ts b/packages/next-auth/tests/middleware.test.ts index b09cc4d3d3..b8a390ee35 100644 --- a/packages/next-auth/tests/middleware.test.ts +++ b/packages/next-auth/tests/middleware.test.ts @@ -1,69 +1,95 @@ -import { NextMiddleware, NextRequest } from "next/server" +import { NextMiddleware } from "next/server" import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware" it("should not match pages as public paths", async () => { const options: NextAuthMiddlewareOptions = { - pages: { signIn: "/", error: "/" }, + pages: { + signIn: "/", + error: "/", + }, secret: "secret", } - const handleMiddleware = withAuth(options) as NextMiddleware - const response = await handleMiddleware( - new NextRequest("http://127.0.0.1/protected/pathA"), - null as any - ) + const nextUrl: any = { + pathname: "/protected/pathA", + search: "", + origin: "http://127.0.0.1", + } + const req: any = { nextUrl, headers: { authorization: "" } } - expect(response?.status).toBe(307) - expect(response?.headers.get("location")).toBe( - "http://localhost/?callbackUrl=%2Fprotected%2FpathA" - ) + const handleMiddleware = withAuth(options) as NextMiddleware + const res = await handleMiddleware(req, null as any) + expect(res).toBeDefined() + expect(res?.status).toBe(307) }) it("should not redirect on public paths", async () => { - const options: NextAuthMiddlewareOptions = { secret: "secret" } - - const req = new NextRequest("http://127.0.0.1/_next/foo") + const options: NextAuthMiddlewareOptions = { + secret: "secret", + } + const nextUrl: any = { + pathname: "/_next/foo", + search: "", + origin: "http://127.0.0.1", + } + const req: any = { nextUrl, headers: { authorization: "" } } const handleMiddleware = withAuth(options) as NextMiddleware const res = await handleMiddleware(req, null as any) expect(res).toBeUndefined() }) -it("should respect NextURL#basePath when redirecting", async () => { - const options: NextAuthMiddlewareOptions = { secret: "secret" } +it("should redirect according to nextUrl basePath", async () => { + const options: NextAuthMiddlewareOptions = { + secret: "secret" + } + const nextUrl: any = { + pathname: "/protected/pathA", + search: "", + origin: "http://127.0.0.1", + basePath: "/custom-base-path", + } + const req: any = { nextUrl, headers: { authorization: "" } } + + const handleMiddleware = withAuth(options) as NextMiddleware + const res = await handleMiddleware(req, null as any) + expect(res).toBeDefined() + expect(res.status).toEqual(307) + expect(res.headers.get('location')).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA") +}) + +it("should redirect according to nextUrl basePath", async () => { + // given + const options: NextAuthMiddlewareOptions = { + secret: "secret" + } const handleMiddleware = withAuth(options) as NextMiddleware - const response1 = await handleMiddleware( - { - nextUrl: { - pathname: "/protected/pathA", - search: "", - origin: "http://127.0.0.1", - basePath: "/custom-base-path", - }, - } as unknown as NextRequest, - null as any - ) - expect(response1?.status).toEqual(307) - expect(response1?.headers.get("location")).toBe( - "http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA" - ) + // when + const res = await handleMiddleware({ + nextUrl: { + pathname: "/protected/pathA", + search: "", + origin: "http://127.0.0.1", + basePath: "/custom-base-path" + }, headers: { authorization: "" } + } as any, null as any) - // Should not redirect when invoked on sign in page + // then + expect(res).toBeDefined() + expect(res.status).toEqual(307) + expect(res.headers.get("location")).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA") - const response2 = await handleMiddleware( - { - nextUrl: { - pathname: "/api/auth/signin", - searchParams: new URLSearchParams({ - callbackUrl: "/custom-base-path/protected/pathA", - }), - origin: "http://127.0.0.1", - basePath: "/custom-base-path", - }, - } as unknown as NextRequest, - null as any - ) + // and when follow redirect + const resFromRedirectedUrl = await handleMiddleware({ + nextUrl: { + pathname: "/api/auth/signin", + search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA", + origin: "http://127.0.0.1", + basePath: "/custom-base-path" + }, headers: { authorization: "" } + } as any, null as any) - expect(response2).toBeUndefined() + // then return sign in page + expect(resFromRedirectedUrl).toBeUndefined() }) diff --git a/packages/next-auth/tests/next.test.ts b/packages/next-auth/tests/next.test.ts deleted file mode 100644 index 85d7be4bf1..0000000000 --- a/packages/next-auth/tests/next.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { mockReqRes, nextHandler } from "./utils" - -it("Missing req.url throws in dev", async () => { - await expect(nextHandler).rejects.toThrow(new Error("Missing url")) -}) - -const configErrorMessage = - "There is a problem with the server configuration. Check the server logs for more information." - -it("Missing req.url returns config error in prod", async () => { - // @ts-expect-error - process.env.NODE_ENV = "production" - const { res, logger } = await nextHandler() - - expect(logger.error).toBeCalledTimes(1) - const error = new Error("Missing url") - expect(logger.error).toBeCalledWith("INVALID_URL", error) - - expect(res.status).toBeCalledWith(400) - expect(res.json).toBeCalledWith({ message: configErrorMessage }) - - // @ts-expect-error - process.env.NODE_ENV = "test" -}) - -it("Missing host throws in dev", async () => { - await expect( - async () => - await nextHandler({ - req: { query: { nextauth: ["session"] } }, - }) - ).rejects.toThrow(Error) -}) - -it("Missing host config error in prod", async () => { - // @ts-expect-error - process.env.NODE_ENV = "production" - const { res, logger } = await nextHandler({ - req: { query: { nextauth: ["session"] } }, - }) - expect(res.status).toBeCalledWith(400) - expect(res.json).toBeCalledWith({ message: configErrorMessage }) - - expect(logger.error).toBeCalledWith("INVALID_URL", new Error("Missing url")) - // @ts-expect-error - process.env.NODE_ENV = "test" -}) - -it("Defined host throws 400 in production if not trusted", async () => { - // @ts-expect-error - process.env.NODE_ENV = "production" - const { res } = await nextHandler({ - req: { headers: { host: "http://localhost" } }, - }) - expect(res.status).toBeCalledWith(400) - // @ts-expect-error - process.env.NODE_ENV = "test" -}) - -it("Defined host throws 400 in production if trusted but invalid URL", async () => { - // @ts-expect-error - process.env.NODE_ENV = "production" - const { res } = await nextHandler({ - req: { headers: { host: "localhost" } }, - options: { trustHost: true }, - }) - expect(res.status).toBeCalledWith(400) - // @ts-expect-error - process.env.NODE_ENV = "test" -}) - -it("Defined host does not throw in production if trusted and valid URL", async () => { - // @ts-expect-error - process.env.NODE_ENV = "production" - const { res } = await nextHandler({ - req: { - url: "/api/auth/session", - headers: { host: "http://localhost" }, - }, - options: { trustHost: true }, - }) - expect(res.status).toBeCalledWith(200) - // @ts-expect-error - expect(JSON.parse(res.send.mock.calls[0][0])).toEqual({}) - // @ts-expect-error - process.env.NODE_ENV = "test" -}) - -it("Use process.env.NEXTAUTH_URL for host if present", async () => { - process.env.NEXTAUTH_URL = "http://localhost" - const { res } = await nextHandler({ - req: { url: "/api/auth/session" }, - }) - expect(res.status).toBeCalledWith(200) - // @ts-expect-error - expect(JSON.parse(res.send.mock.calls[0][0])).toEqual({}) -}) - -it("Redirects if necessary", async () => { - process.env.NEXTAUTH_URL = "http://localhost" - const { res } = await nextHandler({ - req: { - method: "post", - url: "/api/auth/signin/github", - }, - }) - expect(res.status).toBeCalledWith(302) - expect(res.getHeaders()).toEqual({ - location: "http://localhost/api/auth/signin?csrf=true", - "set-cookie": [ - expect.stringMatching( - /next-auth.csrf-token=.*; Path=\/; HttpOnly; SameSite=Lax/ - ), - `next-auth.callback-url=${encodeURIComponent( - process.env.NEXTAUTH_URL - )}; Path=/; HttpOnly; SameSite=Lax`, - ], - }) - - expect(res.send).toBeCalledWith("") -}) - -it("Returns redirect if `X-Auth-Return-Redirect` header is present", async () => { - process.env.NEXTAUTH_URL = "http://localhost" - const { res } = await nextHandler({ - req: { - method: "post", - url: "/api/auth/signin/github", - headers: { "X-Auth-Return-Redirect": "1" }, - }, - }) - - expect(res.status).toBeCalledWith(200) - - expect(res.getHeaders()).toEqual({ - "content-type": "application/json", - "set-cookie": [ - expect.stringMatching( - /next-auth.csrf-token=.*; Path=\/; HttpOnly; SameSite=Lax/ - ), - `next-auth.callback-url=${encodeURIComponent( - process.env.NEXTAUTH_URL - )}; Path=/; HttpOnly; SameSite=Lax`, - ], - }) - - expect(res.send).toBeCalledWith( - JSON.stringify({ url: "http://localhost/api/auth/signin?csrf=true" }) - ) -}) - -it("Should preserve user's `set-cookie` headers", async () => { - const { req, res } = mockReqRes({ - method: "post", - url: "/api/auth/signin/credentials", - headers: { host: "localhost", "X-Auth-Return-Redirect": "1" }, - }) - res.setHeader("set-cookie", ["foo=bar", "bar=baz"]) - - await nextHandler({ req, res }) - - expect(res.getHeaders()).toEqual({ - "content-type": "application/json", - "set-cookie": [ - "foo=bar", - "bar=baz", - expect.stringMatching( - /next-auth.csrf-token=.*; Path=\/; HttpOnly; SameSite=Lax/ - ), - `next-auth.callback-url=${encodeURIComponent( - "http://localhost" - )}; Path=/; HttpOnly; SameSite=Lax`, - ], - }) - - expect(res.send).toBeCalledWith( - JSON.stringify({ url: "http://localhost/api/auth/signin?csrf=true" }) - ) -}) diff --git a/packages/next-auth/tests/utils.ts b/packages/next-auth/tests/utils.ts deleted file mode 100644 index 8e55e20bc1..0000000000 --- a/packages/next-auth/tests/utils.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { createHash } from "node:crypto" -import { IncomingMessage, ServerResponse } from "node:http" -import { Socket } from "node:net" -import type { AuthOptions, LoggerInstance } from "../src" -import type { Adapter } from "../src/adapters" -import { AuthHandler } from "../src/core" - -import NextAuth from "../src/next" - -import type { NextApiRequest, NextApiResponse } from "next" -import { Stream } from "node:stream" - -export function mockLogger(): Record { - return { - error: jest.fn(() => {}), - warn: jest.fn(() => {}), - debug: jest.fn(() => {}), - } -} - -interface HandlerOptions { - prod?: boolean - path?: string - params?: URLSearchParams | Record - requestInit?: RequestInit -} - -export async function handler( - options: AuthOptions, - { prod, path, params, requestInit }: HandlerOptions -) { - // @ts-expect-error - if (prod) process.env.NODE_ENV = "production" - - const url = new URL( - `http://localhost:3000/api/auth/${path ?? "signin"}?${new URLSearchParams( - params ?? {} - )}` - ) - const req = new Request(url, { headers: { host: "" }, ...requestInit }) - const logger = mockLogger() - const response = await AuthHandler(req, { - secret: "secret", - ...options, - logger, - }) - // @ts-expect-error - if (prod) process.env.NODE_ENV = "test" - - return { - res: { - status: response.status, - headers: response.headers, - body: response.body, - redirect: response.headers.get("location"), - html: - response.headers?.get("content-type") === "text/html" - ? await response.clone().text() - : undefined, - }, - log: logger, - } -} - -export function createCSRF() { - const secret = "secret" - const value = "csrf" - const token = createHash("sha256").update(`${value}${secret}`).digest("hex") - - return { - secret, - csrf: { value, token, cookie: `next-auth.csrf-token=${value}|${token}` }, - } -} - -export function mockAdapter(): Adapter { - const adapter: Adapter = { - createVerificationToken: jest.fn(() => {}), - useVerificationToken: jest.fn(() => {}), - getUserByEmail: jest.fn(() => {}), - } as unknown as Adapter - return adapter -} - -export async function nextHandler( - params: { - req?: Partial - res?: Partial - options?: Partial - } = {} -) { - let req = params.req - // @ts-expect-error - let res: NextApiResponse = params.res - if (!params.res) { - ;({ req, res } = mockReqRes(params.req)) - } - - const logger = mockLogger() - // @ts-expect-error - await NextAuth(req, res, { - providers: [], - secret: "secret", - logger, - ...params.options, - }) - - return { req, res, logger } -} - -export function mockReqRes(req?: Partial): { - req: NextApiRequest - res: NextApiResponse -} { - const request = new IncomingMessage(new Socket()) - request.headers = req?.headers ?? {} - request.method = req?.method - request.url = req?.url - - const response = new ServerResponse(request) - // @ts-expect-error - response.status = (code) => (response.statusCode = code) - // @ts-expect-error - response.send = (data) => sendData(request, response, data) - // @ts-expect-error - response.json = (data) => sendJson(response, data) - - const res: NextApiResponse = { - ...response, - // @ts-expect-error - setHeader: jest.spyOn(response, "setHeader"), - // @ts-expect-error - getHeader: jest.spyOn(response, "getHeader"), - // @ts-expect-error - removeHeader: jest.spyOn(response, "removeHeader"), - // @ts-expect-error - status: jest.spyOn(response, "status"), - // @ts-expect-error - send: jest.spyOn(response, "send"), - // @ts-expect-error - json: jest.spyOn(response, "json"), - // @ts-expect-error - end: jest.spyOn(response, "end"), - // @ts-expect-error - getHeaders: jest.spyOn(response, "getHeaders"), - } - - return { req: request as any, res } -} - -// Code below is copied from Next.js -// https://github.com/vercel/next.js/tree/canary/packages/next/server/api-utils -// TODO: Remove - -/** - * Send `any` body to response - * @param req request object - * @param res response object - * @param body of response - */ -function sendData(req: NextApiRequest, res: NextApiResponse, body: any): void { - if (body === null || body === undefined) { - res.end() - return - } - - // strip irrelevant headers/body - if (res.statusCode === 204 || res.statusCode === 304) { - res.removeHeader("Content-Type") - res.removeHeader("Content-Length") - res.removeHeader("Transfer-Encoding") - - if (process.env.NODE_ENV === "development" && body) { - console.warn( - `A body was attempted to be set with a 204 statusCode for ${req.url}, this is invalid and the body was ignored.\n` + - `See more info here https://nextjs.org/docs/messages/invalid-api-status-body` - ) - } - res.end() - return - } - - const contentType = res.getHeader("Content-Type") - - if (body instanceof Stream) { - if (!contentType) { - res.setHeader("Content-Type", "application/octet-stream") - } - body.pipe(res) - return - } - - const isJSONLike = ["object", "number", "boolean"].includes(typeof body) - const stringifiedBody = isJSONLike ? JSON.stringify(body) : body - - if (Buffer.isBuffer(body)) { - if (!contentType) { - res.setHeader("Content-Type", "application/octet-stream") - } - res.setHeader("Content-Length", body.length) - res.end(body) - return - } - - if (isJSONLike) { - res.setHeader("Content-Type", "application/json; charset=utf-8") - } - - res.setHeader("Content-Length", Buffer.byteLength(stringifiedBody)) - res.end(stringifiedBody) -} - -/** - * Send `JSON` object - * @param res response object - * @param jsonBody of data - */ -function sendJson(res: NextApiResponse, jsonBody: any): void { - // Set header to application/json - res.setHeader("Content-Type", "application/json; charset=utf-8") - - // Use send to handle request - res.send(JSON.stringify(jsonBody)) -} From 89758aac00df4f42f65b6af155f058b5239e2c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:15:07 +0100 Subject: [PATCH 2/7] revert line change --- packages/next-auth/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-auth/package.json b/packages/next-auth/package.json index f3c7c6403e..08dc2bb0bd 100644 --- a/packages/next-auth/package.json +++ b/packages/next-auth/package.json @@ -131,4 +131,4 @@ "engines": { "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0" } -} +} \ No newline at end of file From a1bd2f056358abd7774a585642035831ef5f10e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:20:20 +0100 Subject: [PATCH 3/7] replay some TS changes to reduce diff --- packages/next-auth/src/client/_utils.ts | 6 ++--- packages/next-auth/src/core/index.ts | 12 +++++----- packages/next-auth/src/core/init.ts | 4 ++-- packages/next-auth/src/core/lib/assert.ts | 4 ++-- packages/next-auth/src/core/lib/providers.ts | 4 ++-- packages/next-auth/src/core/lib/utils.ts | 4 ++-- packages/next-auth/src/core/pages/index.ts | 4 ++-- .../next-auth/src/core/routes/callback.ts | 4 ++-- .../next-auth/src/core/routes/providers.ts | 4 ++-- packages/next-auth/src/core/routes/session.ts | 6 ++--- packages/next-auth/src/core/routes/signin.ts | 4 ++-- packages/next-auth/src/core/routes/signout.ts | 4 ++-- packages/next-auth/src/core/types.ts | 10 ++++----- packages/next-auth/src/index.ts | 5 ++++- packages/next-auth/src/next/index.ts | 22 +++++++++---------- packages/next-auth/src/next/middleware.ts | 11 ++++++---- packages/next-auth/src/providers/oauth.ts | 2 +- packages/next-auth/src/react/index.tsx | 4 ++-- packages/next-auth/tests/lib.ts | 6 ++--- 19 files changed, 62 insertions(+), 58 deletions(-) diff --git a/packages/next-auth/src/client/_utils.ts b/packages/next-auth/src/client/_utils.ts index 7ccc07761c..79527a2a25 100644 --- a/packages/next-auth/src/client/_utils.ts +++ b/packages/next-auth/src/client/_utils.ts @@ -1,7 +1,7 @@ import type { IncomingMessage } from "http" import type { LoggerInstance, Session } from ".." -export interface NextAuthClientConfig { +export interface AuthClientConfig { baseUrl: string basePath: string baseUrlServer: string @@ -31,7 +31,7 @@ export interface CtxOrReq { */ export async function fetchData( path: string, - __NEXTAUTH: NextAuthClientConfig, + __NEXTAUTH: AuthClientConfig, logger: LoggerInstance, { ctx, req = ctx?.req }: CtxOrReq = {} ): Promise { @@ -50,7 +50,7 @@ export async function fetchData( } } -export function apiBaseUrl(__NEXTAUTH: NextAuthClientConfig) { +export function apiBaseUrl(__NEXTAUTH: AuthClientConfig) { if (typeof window === "undefined") { // Return absolute path when called server side return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}` diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 7d1b39a522..9a892d8f25 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -6,7 +6,7 @@ import { init } from "./init" import { assertConfig } from "./lib/assert" import { SessionStore } from "./lib/cookie" -import type { NextAuthAction, NextAuthOptions } from "./types" +import type { AuthAction, AuthOptions } from "./types" import type { Cookie } from "./lib/cookie" import type { ErrorType } from "./pages/error" import { parse as parseCookie } from "cookie" @@ -19,7 +19,7 @@ export interface RequestInternal { headers?: Record query?: Record body?: Record - action: NextAuthAction + action: AuthAction providerId?: string error?: string } @@ -29,7 +29,7 @@ export interface NextAuthHeader { value: string } -export interface OutgoingResponse< +export interface ResponseInternal< Body extends string | Record | any[] = any > { status?: number @@ -41,7 +41,7 @@ export interface OutgoingResponse< export interface NextAuthHandlerParams { req: Request | RequestInternal - options: NextAuthOptions + options: AuthOptions } async function getBody(req: Request): Promise | undefined> { @@ -63,7 +63,7 @@ async function toInternalRequest( query.nextauth = nextauth return { - action: nextauth[0] as NextAuthAction, + action: nextauth[0] as AuthAction, method: req.method, headers, body: await getBody(req), @@ -79,7 +79,7 @@ async function toInternalRequest( export async function NextAuthHandler< Body extends string | Record | any[] ->(params: NextAuthHandlerParams): Promise> { +>(params: NextAuthHandlerParams): Promise> { const { options: userOptions, req: incomingRequest } = params const req = await toInternalRequest(incomingRequest) diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index b5fa4a72e4..f3fd7d0d9f 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -1,5 +1,5 @@ import { randomBytes, randomUUID } from "crypto" -import { NextAuthOptions } from ".." +import { AuthOptions } from ".." import logger from "../utils/logger" import parseUrl from "../utils/parse-url" import { adapterErrorHandler, eventsErrorHandler } from "./errors" @@ -16,7 +16,7 @@ import type { InternalOptions } from "./types" interface InitParams { host?: string - userOptions: NextAuthOptions + userOptions: AuthOptions providerId?: string action: InternalOptions["action"] /** Callback URL value extracted from the incoming request. */ diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index d1e5bb3533..b6644b8dc4 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -12,7 +12,7 @@ import { defaultCookies } from "./cookie" import type { RequestInternal } from ".." import type { WarningCode } from "../../utils/logger" -import type { NextAuthOptions } from "../types" +import type { AuthOptions } from "../types" type ConfigError = | MissingAPIRoute @@ -40,7 +40,7 @@ function isValidHttpUrl(url: string, baseUrl: string) { * REVIEW: Make some of these and corresponding docs less Next.js specific? */ export function assertConfig(params: { - options: NextAuthOptions + options: AuthOptions req: RequestInternal }): ConfigError | WarningCode[] { const { options, req } = params diff --git a/packages/next-auth/src/core/lib/providers.ts b/packages/next-auth/src/core/lib/providers.ts index 423969990f..892bd374f8 100644 --- a/packages/next-auth/src/core/lib/providers.ts +++ b/packages/next-auth/src/core/lib/providers.ts @@ -2,7 +2,7 @@ import { merge } from "../../utils/merge" import type { InternalProvider } from "../types" import type { - InternalOAuthConfig, + OAuthConfigInternal, OAuthConfig, Provider, } from "../../providers" @@ -59,7 +59,7 @@ function normalizeOAuthOptions( if (!oauthOptions) return const normalized = Object.entries(oauthOptions).reduce< - InternalOAuthConfig> + OAuthConfigInternal> >( (acc, [key, value]) => { if ( diff --git a/packages/next-auth/src/core/lib/utils.ts b/packages/next-auth/src/core/lib/utils.ts index 9d61a7b7a8..86e40220d5 100644 --- a/packages/next-auth/src/core/lib/utils.ts +++ b/packages/next-auth/src/core/lib/utils.ts @@ -1,6 +1,6 @@ import { createHash } from "crypto" -import type { NextAuthOptions } from "../.." +import type { AuthOptions } from "../.." import type { InternalOptions } from "../types" import type { InternalUrl } from "../../utils/parse-url" @@ -29,7 +29,7 @@ export function hashToken(token: string, options: InternalOptions<"email">) { * 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: { - userOptions: NextAuthOptions + userOptions: AuthOptions url: InternalUrl }) { const { userOptions, url } = params diff --git a/packages/next-auth/src/core/pages/index.ts b/packages/next-auth/src/core/pages/index.ts index 9812497aa0..6938a4e016 100644 --- a/packages/next-auth/src/core/pages/index.ts +++ b/packages/next-auth/src/core/pages/index.ts @@ -6,7 +6,7 @@ import ErrorPage from "./error" import css from "../../css" import type { InternalOptions } from "../types" -import type { RequestInternal, OutgoingResponse } from ".." +import type { RequestInternal, ResponseInternal } from ".." import type { Cookie } from "../lib/cookie" import type { ErrorType } from "./error" @@ -27,7 +27,7 @@ type RenderPageParams = { export default function renderPage(params: RenderPageParams) { const { url, theme, query, cookies } = params - function send({ html, title, status }: any): OutgoingResponse { + function send({ html, title, status }: any): ResponseInternal { return { cookies, status, diff --git a/packages/next-auth/src/core/routes/callback.ts b/packages/next-auth/src/core/routes/callback.ts index c42bfa3f9d..b7008d6453 100644 --- a/packages/next-auth/src/core/routes/callback.ts +++ b/packages/next-auth/src/core/routes/callback.ts @@ -4,7 +4,7 @@ import { hashToken } from "../lib/utils" import getAdapterUserFromEmail from "../lib/email/getUserFromEmail" import type { InternalOptions } from "../types" -import type { RequestInternal, OutgoingResponse } from ".." +import type { RequestInternal, ResponseInternal } from ".." import type { Cookie, SessionStore } from "../lib/cookie" import type { User } from "../.." import type { AdapterSession } from "../../adapters" @@ -18,7 +18,7 @@ export default async function callback(params: { headers: RequestInternal["headers"] cookies: RequestInternal["cookies"] sessionStore: SessionStore -}): Promise { +}): Promise { const { options, query, body, method, headers, sessionStore } = params const { provider, diff --git a/packages/next-auth/src/core/routes/providers.ts b/packages/next-auth/src/core/routes/providers.ts index f946538c5f..9ce34acd44 100644 --- a/packages/next-auth/src/core/routes/providers.ts +++ b/packages/next-auth/src/core/routes/providers.ts @@ -1,4 +1,4 @@ -import type { OutgoingResponse } from ".." +import type { ResponseInternal } from ".." import type { InternalProvider } from "../types" export interface PublicProvider { @@ -16,7 +16,7 @@ export interface PublicProvider { */ export default function providers( providers: InternalProvider[] -): OutgoingResponse> { +): ResponseInternal> { return { headers: [{ key: "Content-Type", value: "application/json" }], body: providers.reduce>( diff --git a/packages/next-auth/src/core/routes/session.ts b/packages/next-auth/src/core/routes/session.ts index 8989bc7138..73caccdd9b 100644 --- a/packages/next-auth/src/core/routes/session.ts +++ b/packages/next-auth/src/core/routes/session.ts @@ -2,7 +2,7 @@ import { fromDate } from "../lib/utils" import type { Adapter } from "../../adapters" import type { InternalOptions } from "../types" -import type { OutgoingResponse } from ".." +import type { ResponseInternal } from ".." import type { Session } from "../.." import type { SessionStore } from "../lib/cookie" @@ -18,7 +18,7 @@ interface SessionParams { export default async function session( params: SessionParams -): Promise> { +): Promise> { const { options, sessionStore } = params const { adapter, @@ -29,7 +29,7 @@ export default async function session( session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, } = options - const response: OutgoingResponse = { + const response: ResponseInternal = { body: {}, headers: [{ key: "Content-Type", value: "application/json" }], cookies: [], diff --git a/packages/next-auth/src/core/routes/signin.ts b/packages/next-auth/src/core/routes/signin.ts index 3b85abc52c..201f279101 100644 --- a/packages/next-auth/src/core/routes/signin.ts +++ b/packages/next-auth/src/core/routes/signin.ts @@ -1,7 +1,7 @@ import getAuthorizationUrl from "../lib/oauth/authorization-url" import emailSignin from "../lib/email/signin" import getAdapterUserFromEmail from "../lib/email/getUserFromEmail" -import type { RequestInternal, OutgoingResponse } from ".." +import type { RequestInternal, ResponseInternal } from ".." import type { InternalOptions } from "../types" import type { Account } from "../.." @@ -10,7 +10,7 @@ export default async function signin(params: { options: InternalOptions<"oauth" | "email"> query: RequestInternal["query"] body: RequestInternal["body"] -}): Promise { +}): Promise { const { options, query, body } = params const { url, callbacks, logger, provider } = options diff --git a/packages/next-auth/src/core/routes/signout.ts b/packages/next-auth/src/core/routes/signout.ts index 69648aabcd..5078d53fb5 100644 --- a/packages/next-auth/src/core/routes/signout.ts +++ b/packages/next-auth/src/core/routes/signout.ts @@ -1,13 +1,13 @@ import type { Adapter } from "../../adapters" import type { InternalOptions } from "../types" -import type { OutgoingResponse } from ".." +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 { +}): Promise { const { options, sessionStore } = params const { adapter, events, jwt, callbackUrl, logger, session } = options diff --git a/packages/next-auth/src/core/types.ts b/packages/next-auth/src/core/types.ts index 8f192d9507..3213792197 100644 --- a/packages/next-auth/src/core/types.ts +++ b/packages/next-auth/src/core/types.ts @@ -5,7 +5,7 @@ import type { ProviderType, EmailConfig, CredentialsConfig, - InternalOAuthConfig, + OAuthConfigInternal, } from "../providers" import type { TokenSetParameters } from "openid-client" import type { JWT, JWTOptions } from "../jwt" @@ -25,7 +25,7 @@ export type { LoggerInstance } * * [Documentation](https://next-auth.js.org/configuration/options#options) */ -export interface NextAuthOptions { +export interface AuthOptions { /** * An array of authentication providers for signing in * (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order. @@ -491,7 +491,7 @@ export interface User extends DefaultUser {} /** @internal */ export type InternalProvider = (T extends "oauth" - ? InternalOAuthConfig + ? OAuthConfigInternal : T extends "email" ? EmailConfig : T extends "credentials" @@ -501,7 +501,7 @@ export type InternalProvider = (T extends "oauth" callbackUrl: string } -export type NextAuthAction = +export type AuthAction = | "providers" | "session" | "csrf" @@ -523,7 +523,7 @@ export interface InternalOptions< * @default "http://localhost:3000/api/auth" */ url: InternalUrl - action: NextAuthAction + action: AuthAction provider: InternalProvider csrfToken?: string csrfTokenVerified?: boolean diff --git a/packages/next-auth/src/index.ts b/packages/next-auth/src/index.ts index 401838bba1..2733796b16 100644 --- a/packages/next-auth/src/index.ts +++ b/packages/next-auth/src/index.ts @@ -1,6 +1,9 @@ export * from "./core/types" -export type { RequestInternal, OutgoingResponse } from "./core" +export type { + RequestInternal, + ResponseInternal as OutgoingResponse, +} from "./core" export * from "./next" export { default } from "./next" diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 4e4119c610..3853aac61f 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -7,10 +7,10 @@ import type { NextApiRequest, NextApiResponse, } from "next" -import type { NextAuthOptions, Session } from ".." +import type { AuthOptions, Session } from ".." import type { CallbacksOptions, - NextAuthAction, + AuthAction, NextAuthRequest, NextAuthResponse, } from "../core/types" @@ -18,7 +18,7 @@ import type { async function NextAuthNextHandler( req: NextApiRequest, res: NextApiResponse, - options: NextAuthOptions + options: AuthOptions ) { const { nextauth, ...query } = req.query @@ -33,7 +33,7 @@ async function NextAuthNextHandler( cookies: req.cookies, headers: req.headers, method: req.method, - action: nextauth?.[0] as NextAuthAction, + action: nextauth?.[0] as AuthAction, providerId: nextauth?.[1], error: (req.query.error as string | undefined) ?? nextauth?.[1], }, @@ -61,18 +61,16 @@ async function NextAuthNextHandler( return res.send(handler.body) } -function NextAuth(options: NextAuthOptions): any +function NextAuth(options: AuthOptions): any function NextAuth( req: NextApiRequest, res: NextApiResponse, - options: NextAuthOptions + options: AuthOptions ): any /** The main entry point to next-auth */ function NextAuth( - ...args: - | [NextAuthOptions] - | [NextApiRequest, NextApiResponse, NextAuthOptions] + ...args: [AuthOptions] | [NextApiRequest, NextApiResponse, AuthOptions] ) { if (args.length === 1) { return async (req: NextAuthRequest, res: NextAuthResponse) => @@ -87,8 +85,8 @@ export default NextAuth let experimentalWarningShown = false let experimentalRSCWarningShown = false -type GetServerSessionOptions = Partial> & { - callbacks?: Omit & { +type GetServerSessionOptions = Partial> & { + callbacks?: Omit & { session?: (...args: Parameters) => any } } @@ -130,7 +128,7 @@ export async function unstable_getServerSession< experimentalRSCWarningShown = true } - let req, res, options: NextAuthOptions + let req, res, options: AuthOptions if (isRSC) { options = Object.assign({}, args[0], { providers: [] }) diff --git a/packages/next-auth/src/next/middleware.ts b/packages/next-auth/src/next/middleware.ts index 0b958149a3..881902087b 100644 --- a/packages/next-auth/src/next/middleware.ts +++ b/packages/next-auth/src/next/middleware.ts @@ -1,5 +1,5 @@ import type { NextMiddleware, NextFetchEvent } from "next/server" -import type { Awaitable, CookieOption, NextAuthOptions } from ".." +import type { Awaitable, CookieOption, AuthOptions } from ".." import type { JWT, JWTOptions } from "../jwt" import { NextResponse, NextRequest } from "next/server" @@ -20,7 +20,7 @@ export interface NextAuthMiddlewareOptions { * --- * [Documentation](https://next-auth.js.org/configuration/pages) */ - pages?: NextAuthOptions["pages"] + pages?: AuthOptions["pages"] /** * You can override the default cookie names and options for any of the cookies @@ -38,7 +38,7 @@ export interface NextAuthMiddlewareOptions { */ cookies?: Partial< Record< - keyof Pick, + keyof Pick, Omit > > @@ -146,7 +146,10 @@ async function handleMiddleware( // the user is not logged in, redirect to the sign-in page const signInUrl = new URL(`${basePath}${signInPage}`, origin) - signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`) + signInUrl.searchParams.append( + "callbackUrl", + `${basePath}${pathname}${search}` + ) return NextResponse.redirect(signInUrl) } diff --git a/packages/next-auth/src/providers/oauth.ts b/packages/next-auth/src/providers/oauth.ts index c0ae9fe3b0..c476279b12 100644 --- a/packages/next-auth/src/providers/oauth.ts +++ b/packages/next-auth/src/providers/oauth.ts @@ -160,7 +160,7 @@ export interface OAuthConfig

extends CommonProviderOptions, PartialIssuer { } /** @internal */ -export interface InternalOAuthConfig

+export interface OAuthConfigInternal

extends Omit, "authorization" | "token" | "userinfo"> { authorization?: AuthorizationEndpointHandler token?: TokenEndpointHandler diff --git a/packages/next-auth/src/react/index.tsx b/packages/next-auth/src/react/index.tsx index 07f1f199c9..bab02ec8a4 100644 --- a/packages/next-auth/src/react/index.tsx +++ b/packages/next-auth/src/react/index.tsx @@ -18,7 +18,7 @@ import { apiBaseUrl, fetchData, now, - NextAuthClientConfig, + AuthClientConfig, } from "../client/_utils" import type { @@ -46,7 +46,7 @@ export * from "./types" // relative URLs are valid in that context and so defaults to empty. // 2. When invoked server side the value is picked up from an environment // variable and defaults to 'http://localhost:3000'. -const __NEXTAUTH: NextAuthClientConfig = { +const __NEXTAUTH: AuthClientConfig = { baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin, basePath: parseUrl(process.env.NEXTAUTH_URL).path, baseUrlServer: parseUrl( diff --git a/packages/next-auth/tests/lib.ts b/packages/next-auth/tests/lib.ts index 7d4a977390..696eca0bf6 100644 --- a/packages/next-auth/tests/lib.ts +++ b/packages/next-auth/tests/lib.ts @@ -1,6 +1,6 @@ import { createHash } from "crypto" import { NextAuthHandler } from "../src/core" -import type { LoggerInstance, NextAuthOptions } from "../src" +import type { LoggerInstance, AuthOptions } from "../src" import type { Adapter } from "../src/adapters" export const mockLogger: () => LoggerInstance = () => ({ @@ -17,7 +17,7 @@ interface HandlerOptions { } export async function handler( - options: NextAuthOptions, + options: AuthOptions, { prod, path, params, requestInit }: HandlerOptions ) { // @ts-expect-error @@ -63,6 +63,6 @@ export function mockAdapter(): Adapter { createVerificationToken: jest.fn(() => {}), useVerificationToken: jest.fn(() => {}), getUserByEmail: jest.fn(() => {}), - } + } as unknown as Adapter return adapter } From f9458967046a5cd3f2e0ae06cedcc2b49a7877b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:33:54 +0100 Subject: [PATCH 4/7] fix tests --- packages/next-auth/tests/pkce-handler.test.ts | 47 +++++++---------- .../next-auth/tests/state-handler.test.ts | 50 +++++++------------ 2 files changed, 35 insertions(+), 62 deletions(-) diff --git a/packages/next-auth/tests/pkce-handler.test.ts b/packages/next-auth/tests/pkce-handler.test.ts index 45bfddaaf3..a780853b01 100644 --- a/packages/next-auth/tests/pkce-handler.test.ts +++ b/packages/next-auth/tests/pkce-handler.test.ts @@ -1,20 +1,15 @@ -import { mockLogger } from "./utils" +import { mockLogger } from "./lib" import type { InternalOptions, LoggerInstance, InternalProvider, CallbacksOptions, - Account, Awaitable, - Profile, - Session, - User, CookiesOptions, } from "../src" import { createPKCE } from "../src/core/lib/oauth/pkce-handler" import { InternalUrl } from "../src/utils/parse-url" import { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "../src/jwt" -import { CredentialInput } from "../src/providers" let logger: LoggerInstance let url: InternalUrl @@ -42,6 +37,9 @@ beforeEach(() => { signinUrl: "/", callbackUrl: "/", checks: ["pkce", "state"], + profile() { + return { id: "", name: "", email: "" } + }, } jwt = { @@ -56,35 +54,16 @@ beforeEach(() => { } callbacks = { - signIn: function (params: { - user: User - account: Account - profile: Profile & Record - email: { verificationRequest?: boolean | undefined } - credentials?: Record | undefined - }): Awaitable { + signIn: function () { throw new Error("Function not implemented.") }, - redirect: function (params: { - url: string - baseUrl: string - }): Awaitable { + redirect: function () { throw new Error("Function not implemented.") }, - session: function (params: { - session: Session - user: User - token: JWT - }): Awaitable { + session: function () { throw new Error("Function not implemented.") }, - jwt: function (params: { - token: JWT - user?: User | undefined - account?: Account | undefined - profile?: Profile | undefined - isNewUser?: boolean | undefined - }): Awaitable { + jwt: function () { throw new Error("Function not implemented.") }, } @@ -100,12 +79,20 @@ beforeEach(() => { options = { url, + adapter: undefined, action: "session", provider, secret: "", debug: false, logger, - session: { strategy: "jwt", maxAge: 0, updateAge: 0 }, + session: { + strategy: "jwt", + maxAge: 0, + updateAge: 0, + generateSessionToken() { + return "" + }, + }, pages: {}, jwt, events: {}, diff --git a/packages/next-auth/tests/state-handler.test.ts b/packages/next-auth/tests/state-handler.test.ts index 38bdc4ab86..b8f9b5f834 100644 --- a/packages/next-auth/tests/state-handler.test.ts +++ b/packages/next-auth/tests/state-handler.test.ts @@ -1,20 +1,14 @@ -import { mockLogger } from "./utils" +import { mockLogger } from "./lib" import type { InternalOptions, LoggerInstance, InternalProvider, CallbacksOptions, - Account, - Awaitable, - Profile, - Session, - User, CookiesOptions, } from "../src" import { createState } from "../src/core/lib/oauth/state-handler" import { InternalUrl } from "../src/utils/parse-url" -import { JWT, JWTOptions, encode, decode } from "../src/jwt" -import { CredentialInput } from "../src/providers" +import { JWTOptions, encode, decode } from "../src/jwt" let logger: LoggerInstance let url: InternalUrl @@ -42,6 +36,9 @@ beforeEach(() => { signinUrl: "/", callbackUrl: "/", checks: ["pkce", "state"], + profile() { + return { id: "", name: "", email: "" } + }, } jwt = { @@ -52,35 +49,16 @@ beforeEach(() => { } callbacks = { - signIn: function (params: { - user: User - account: Account - profile: Profile & Record - email: { verificationRequest?: boolean | undefined } - credentials?: Record | undefined - }): Awaitable { + signIn: function () { throw new Error("Function not implemented.") }, - redirect: function (params: { - url: string - baseUrl: string - }): Awaitable { + redirect: function () { throw new Error("Function not implemented.") }, - session: function (params: { - session: Session - user: User - token: JWT - }): Awaitable { + session: function () { throw new Error("Function not implemented.") }, - jwt: function (params: { - token: JWT - user?: User | undefined - account?: Account | undefined - profile?: Profile | undefined - isNewUser?: boolean | undefined - }): Awaitable { + jwt: function () { throw new Error("Function not implemented.") }, } @@ -96,12 +74,20 @@ beforeEach(() => { options = { url, + adapter: undefined, action: "session", provider, secret: "", debug: false, logger, - session: { strategy: "jwt", maxAge: 0, updateAge: 0 }, + session: { + strategy: "jwt", + maxAge: 0, + updateAge: 0, + generateSessionToken() { + return "" + }, + }, pages: {}, jwt, events: {}, From 0f65137f0e910245dc7dfef39d47616af8128587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:37:19 +0100 Subject: [PATCH 5/7] revert more renames --- packages/next-auth/src/core/index.ts | 12 ++++---- packages/next-auth/src/core/init.ts | 30 ++++++++++---------- packages/next-auth/src/core/lib/providers.ts | 16 +++++------ packages/next-auth/src/core/lib/utils.ts | 8 +++--- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 9a892d8f25..6e985878f3 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -80,13 +80,13 @@ async function toInternalRequest( export async function NextAuthHandler< Body extends string | Record | any[] >(params: NextAuthHandlerParams): Promise> { - const { options: userOptions, req: incomingRequest } = params + const { options: authOptions, req: incomingRequest } = params const req = await toInternalRequest(incomingRequest) - setLogger(userOptions.logger, userOptions.debug) + setLogger(authOptions.logger, authOptions.debug) - const assertionResult = assertConfig({ options: userOptions, req }) + const assertionResult = assertConfig({ options: authOptions, req }) if (Array.isArray(assertionResult)) { assertionResult.forEach(logger.warn) @@ -103,7 +103,7 @@ export async function NextAuthHandler< body: { message } as any, } } - const { pages, theme } = userOptions + const { pages, theme } = authOptions const authOnErrorPage = pages?.error && req.query?.callbackUrl?.startsWith(pages.error) @@ -129,7 +129,7 @@ export async function NextAuthHandler< const { action, providerId, error, method = "GET" } = req const { options, cookies } = await init({ - userOptions, + authOptions: authOptions, action, providerId, host: req.host, @@ -275,7 +275,7 @@ export async function NextAuthHandler< } break case "_log": - if (userOptions.logger) { + if (authOptions.logger) { try { const { code, level, ...metadata } = req.body ?? {} logger[level](code, metadata) diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index f3fd7d0d9f..aa8d6597dd 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -1,7 +1,6 @@ import { randomBytes, randomUUID } from "crypto" import { AuthOptions } from ".." import logger from "../utils/logger" -import parseUrl from "../utils/parse-url" import { adapterErrorHandler, eventsErrorHandler } from "./errors" import parseProviders from "./lib/providers" import { createSecret } from "./lib/utils" @@ -13,10 +12,11 @@ import { createCallbackUrl } from "./lib/callback-url" import { RequestInternal } from "." import type { InternalOptions } from "./types" +import parseUrl from "../utils/parse-url" interface InitParams { host?: string - userOptions: AuthOptions + authOptions: AuthOptions providerId?: string action: InternalOptions["action"] /** Callback URL value extracted from the incoming request. */ @@ -30,7 +30,7 @@ interface InitParams { /** Initialize all internal options and cookies. */ export async function init({ - userOptions, + authOptions, providerId, action, host, @@ -44,10 +44,10 @@ export async function init({ }> { const url = parseUrl(host) - const secret = createSecret({ userOptions, url }) + const secret = createSecret({ authOptions: authOptions, url }) const { providers, provider } = parseProviders({ - providers: userOptions.providers, + providers: authOptions.providers, url, providerId, }) @@ -66,8 +66,8 @@ export async function init({ buttonText: "", }, // Custom options override defaults - ...userOptions, - // These computed settings can have values in userOptions but we override them + ...authOptions, + // These computed settings can have values in authOptions but we override them // and are request-specific. url, action, @@ -75,24 +75,24 @@ export async function init({ provider, cookies: { ...cookie.defaultCookies( - userOptions.useSecureCookies ?? url.base.startsWith("https://") + authOptions.useSecureCookies ?? url.base.startsWith("https://") ), // Allow user cookie options to override any cookie settings above - ...userOptions.cookies, + ...authOptions.cookies, }, secret, providers, // Session options session: { // If no adapter specified, force use of JSON Web Tokens (stateless) - strategy: userOptions.adapter ? "database" : "jwt", + strategy: authOptions.adapter ? "database" : "jwt", maxAge, updateAge: 24 * 60 * 60, generateSessionToken: () => { // Use `randomUUID` if available. (Node 15.6+) return randomUUID?.() ?? randomBytes(32).toString("hex") }, - ...userOptions.session, + ...authOptions.session, }, // JWT options jwt: { @@ -100,13 +100,13 @@ export async function init({ maxAge, // same as session maxAge, encode: jwt.encode, decode: jwt.decode, - ...userOptions.jwt, + ...authOptions.jwt, }, // Event messages - events: eventsErrorHandler(userOptions.events ?? {}, logger), - adapter: adapterErrorHandler(userOptions.adapter, logger), + events: eventsErrorHandler(authOptions.events ?? {}, logger), + adapter: adapterErrorHandler(authOptions.adapter, logger), // Callback functions - callbacks: { ...defaultCallbacks, ...userOptions.callbacks }, + callbacks: { ...defaultCallbacks, ...authOptions.callbacks }, logger, callbackUrl: url.origin, } diff --git a/packages/next-auth/src/core/lib/providers.ts b/packages/next-auth/src/core/lib/providers.ts index 892bd374f8..60b051cad2 100644 --- a/packages/next-auth/src/core/lib/providers.ts +++ b/packages/next-auth/src/core/lib/providers.ts @@ -23,20 +23,20 @@ export default function parseProviders(params: { const { url, providerId } = params const providers = params.providers.map( - ({ options: userOptions, ...rest }) => { + ({ options: authOptions, ...rest }) => { if (rest.type === "oauth") { const normalizedOptions = normalizeOAuthOptions(rest) - const normalizedUserOptions = normalizeOAuthOptions(userOptions, true) - const id = normalizedUserOptions?.id ?? rest.id + const normalizedauthOptions = normalizeOAuthOptions(authOptions, true) + const id = normalizedauthOptions?.id ?? rest.id return merge(normalizedOptions, { - ...normalizedUserOptions, + ...normalizedauthOptions, signinUrl: `${url}/signin/${id}`, callbackUrl: `${url}/callback/${id}`, }) } - const id = (userOptions?.id as string) ?? rest.id + const id = (authOptions?.id as string) ?? rest.id return merge(rest, { - ...userOptions, + ...authOptions, signinUrl: `${url}/signin/${id}`, callbackUrl: `${url}/callback/${id}`, }) @@ -54,7 +54,7 @@ export default function parseProviders(params: { */ function normalizeOAuthOptions( oauthOptions?: Partial> | Record, - isUserOptions = false + isauthOptions = false ) { if (!oauthOptions) return @@ -81,7 +81,7 @@ function normalizeOAuthOptions( {} as any ) - if (!isUserOptions && !normalized.version?.startsWith("1.")) { + if (!isauthOptions && !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. diff --git a/packages/next-auth/src/core/lib/utils.ts b/packages/next-auth/src/core/lib/utils.ts index 86e40220d5..c8296a766b 100644 --- a/packages/next-auth/src/core/lib/utils.ts +++ b/packages/next-auth/src/core/lib/utils.ts @@ -29,16 +29,16 @@ export function hashToken(token: string, options: InternalOptions<"email">) { * 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: { - userOptions: AuthOptions + authOptions: AuthOptions url: InternalUrl }) { - const { userOptions, url } = params + const { authOptions, url } = params return ( - userOptions.secret ?? + authOptions.secret ?? // TODO: Remove falling back to default secret, and error in dev if one isn't provided createHash("sha256") - .update(JSON.stringify({ ...url, ...userOptions })) + .update(JSON.stringify({ ...url, ...authOptions })) .digest("hex") ) } From 0dadcd431afab58a0711c04adc3901da753bc18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:42:47 +0100 Subject: [PATCH 6/7] revert renames --- packages/next-auth/src/core/index.ts | 2 +- packages/next-auth/src/core/lib/providers.ts | 16 ++++++++-------- packages/next-auth/src/index.ts | 1 + packages/next-auth/src/jwt/index.ts | 2 +- packages/next-auth/src/next/index.ts | 12 ++++++------ packages/next-auth/tests/lib.ts | 4 ++-- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 6e985878f3..677cfa2c3b 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -77,7 +77,7 @@ async function toInternalRequest( return req } -export async function NextAuthHandler< +export async function AuthHandler< Body extends string | Record | any[] >(params: NextAuthHandlerParams): Promise> { const { options: authOptions, req: incomingRequest } = params diff --git a/packages/next-auth/src/core/lib/providers.ts b/packages/next-auth/src/core/lib/providers.ts index 60b051cad2..892bd374f8 100644 --- a/packages/next-auth/src/core/lib/providers.ts +++ b/packages/next-auth/src/core/lib/providers.ts @@ -23,20 +23,20 @@ export default function parseProviders(params: { const { url, providerId } = params const providers = params.providers.map( - ({ options: authOptions, ...rest }) => { + ({ options: userOptions, ...rest }) => { if (rest.type === "oauth") { const normalizedOptions = normalizeOAuthOptions(rest) - const normalizedauthOptions = normalizeOAuthOptions(authOptions, true) - const id = normalizedauthOptions?.id ?? rest.id + const normalizedUserOptions = normalizeOAuthOptions(userOptions, true) + const id = normalizedUserOptions?.id ?? rest.id return merge(normalizedOptions, { - ...normalizedauthOptions, + ...normalizedUserOptions, signinUrl: `${url}/signin/${id}`, callbackUrl: `${url}/callback/${id}`, }) } - const id = (authOptions?.id as string) ?? rest.id + const id = (userOptions?.id as string) ?? rest.id return merge(rest, { - ...authOptions, + ...userOptions, signinUrl: `${url}/signin/${id}`, callbackUrl: `${url}/callback/${id}`, }) @@ -54,7 +54,7 @@ export default function parseProviders(params: { */ function normalizeOAuthOptions( oauthOptions?: Partial> | Record, - isauthOptions = false + isUserOptions = false ) { if (!oauthOptions) return @@ -81,7 +81,7 @@ function normalizeOAuthOptions( {} as any ) - if (!isauthOptions && !normalized.version?.startsWith("1.")) { + 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. diff --git a/packages/next-auth/src/index.ts b/packages/next-auth/src/index.ts index 2733796b16..fdb9e17020 100644 --- a/packages/next-auth/src/index.ts +++ b/packages/next-auth/src/index.ts @@ -1,4 +1,5 @@ export * from "./core/types" +export type { AuthOptions as NextAuthOptions } from "./core/types" export type { RequestInternal, diff --git a/packages/next-auth/src/jwt/index.ts b/packages/next-auth/src/jwt/index.ts index 03df8e8a10..aec2e0492a 100644 --- a/packages/next-auth/src/jwt/index.ts +++ b/packages/next-auth/src/jwt/index.ts @@ -94,7 +94,7 @@ export async function getToken( const authorizationHeader = req.headers instanceof Headers ? req.headers.get("authorization") - : req.headers.authorization + : req.headers?.authorization if (!token && authorizationHeader?.split(" ")[0] === "Bearer") { const urlEncodedToken = authorizationHeader.split(" ")[1] diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 3853aac61f..b3f57b6674 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -1,4 +1,4 @@ -import { NextAuthHandler } from "../core" +import { AuthHandler } from "../core" import { detectHost } from "../utils/detect-host" import { setCookie } from "./utils" @@ -15,7 +15,7 @@ import type { NextAuthResponse, } from "../core/types" -async function NextAuthNextHandler( +async function NextAuthHandler( req: NextApiRequest, res: NextApiResponse, options: AuthOptions @@ -25,7 +25,7 @@ async function NextAuthNextHandler( options.secret = options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET - const handler = await NextAuthHandler({ + const handler = await AuthHandler({ req: { host: detectHost(req.headers["x-forwarded-host"]), body: req.body, @@ -74,10 +74,10 @@ function NextAuth( ) { if (args.length === 1) { return async (req: NextAuthRequest, res: NextAuthResponse) => - await NextAuthNextHandler(req, res, args[0]) + await NextAuthHandler(req, res, args[0]) } - return NextAuthNextHandler(args[0], args[1], args[2]) + return NextAuthHandler(args[0], args[1], args[2]) } export default NextAuth @@ -151,7 +151,7 @@ export async function unstable_getServerSession< options.secret = options.secret ?? process.env.NEXTAUTH_SECRET - const session = await NextAuthHandler({ + const session = await AuthHandler({ options, req: { host: detectHost(req.headers["x-forwarded-host"]), diff --git a/packages/next-auth/tests/lib.ts b/packages/next-auth/tests/lib.ts index 696eca0bf6..2c6405acb0 100644 --- a/packages/next-auth/tests/lib.ts +++ b/packages/next-auth/tests/lib.ts @@ -1,5 +1,5 @@ import { createHash } from "crypto" -import { NextAuthHandler } from "../src/core" +import { AuthHandler } from "../src/core" import type { LoggerInstance, AuthOptions } from "../src" import type { Adapter } from "../src/adapters" @@ -30,7 +30,7 @@ export async function handler( ) const req = new Request(url, { headers: { host: "" }, ...requestInit }) const logger = mockLogger() - const response = await NextAuthHandler({ + const response = await AuthHandler({ req, options: { secret: "secret", ...options, logger }, }) From 367fe049b5e3ef57324ad834e62a60f1bb279fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 21 Dec 2022 03:47:21 +0100 Subject: [PATCH 7/7] fix test, cleanup --- packages/next-auth/src/core/index.ts | 2 +- packages/next-auth/src/core/init.ts | 2 +- packages/next-auth/tests/getServerSession.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 677cfa2c3b..cba0f212f0 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -129,7 +129,7 @@ export async function AuthHandler< const { action, providerId, error, method = "GET" } = req const { options, cookies } = await init({ - authOptions: authOptions, + authOptions, action, providerId, host: req.host, diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index aa8d6597dd..2fd8491842 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -44,7 +44,7 @@ export async function init({ }> { const url = parseUrl(host) - const secret = createSecret({ authOptions: authOptions, url }) + const secret = createSecret({ authOptions, url }) const { providers, provider } = parseProviders({ providers: authOptions.providers, diff --git a/packages/next-auth/tests/getServerSession.test.ts b/packages/next-auth/tests/getServerSession.test.ts index 64c30cdc45..6643fa9bdd 100644 --- a/packages/next-auth/tests/getServerSession.test.ts +++ b/packages/next-auth/tests/getServerSession.test.ts @@ -83,7 +83,7 @@ describe("Return correct data", () => { }) it("Should return null if there is no session", async () => { - const spy = jest.spyOn(core, "NextAuthHandler") + const spy = jest.spyOn(core, "AuthHandler") // @ts-expect-error spy.mockReturnValue({ body: {} }) @@ -109,7 +109,7 @@ describe("Return correct data", () => { }, } - const spy = jest.spyOn(core, "NextAuthHandler") + const spy = jest.spyOn(core, "AuthHandler") // @ts-expect-error spy.mockReturnValue(mockedResponse)