From f2cfbd160ddfd5005b2d98f5e269989152d5bd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 13:08:55 +0100 Subject: [PATCH 01/26] rename `host` to `origin` internally --- packages/next-auth/src/core/index.ts | 4 ++-- packages/next-auth/src/core/init.ts | 6 +++--- packages/next-auth/src/core/lib/assert.ts | 4 ++-- packages/next-auth/src/utils/web.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 7397febdad..81f856c1ca 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -12,7 +12,7 @@ import type { ErrorType } from "./pages/error" export interface RequestInternal { /** @default "http://localhost:3000" */ - host?: string + origin?: string method?: string cookies?: Partial> headers?: Record @@ -96,7 +96,7 @@ async function AuthHandlerInternal< userOptions, action, providerId, - host: req.host, + origin: req.origin, callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl, csrfToken: req.body?.csrfToken, cookies: req.cookies, diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index f3fd7d0d9f..cc4c77f091 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -15,7 +15,7 @@ import { RequestInternal } from "." import type { InternalOptions } from "./types" interface InitParams { - host?: string + origin?: string userOptions: AuthOptions providerId?: string action: InternalOptions["action"] @@ -33,7 +33,7 @@ export async function init({ userOptions, providerId, action, - host, + origin, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, @@ -42,7 +42,7 @@ export async function init({ options: InternalOptions cookies: cookie.Cookie[] }> { - const url = parseUrl(host) + const url = parseUrl(origin) const secret = createSecret({ userOptions, url }) diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index b6644b8dc4..de3088f3f4 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -48,7 +48,7 @@ export function assertConfig(params: { const warnings: WarningCode[] = [] if (!warned) { - if (!req.host) warnings.push("NEXTAUTH_URL") + if (!req.origin) warnings.push("NEXTAUTH_URL") // TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV` if (!options.secret && process.env.NODE_ENV !== "production") @@ -70,7 +70,7 @@ export function assertConfig(params: { const callbackUrlParam = req.query?.callbackUrl as string | undefined - const url = parseUrl(req.host) + const url = parseUrl(req.origin) if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) { return new InvalidCallbackUrl( diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index d071ba0003..6ce17266a2 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -63,7 +63,7 @@ export async function toInternalRequest( cookies: cookies, providerId: nextauth[1], error: url.searchParams.get("error") ?? undefined, - host: new URL(req.url).origin, + origin: url.origin, query, } } From d43015ead1274586d0fb4bd6afe6edf34afba00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 13:10:21 +0100 Subject: [PATCH 02/26] rename `userOptions` to `authOptions` internally --- packages/next-auth/src/core/index.ts | 12 +++++------ packages/next-auth/src/core/init.ts | 26 ++++++++++++------------ packages/next-auth/src/core/lib/utils.ts | 8 ++++---- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 81f856c1ca..589df84330 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -47,10 +47,10 @@ async function AuthHandlerInternal< /** REVIEW: Is this the best way to skip parsing the body in Node.js? */ parsedBody?: any }): Promise> { - const { options: userOptions, req } = params - setLogger(userOptions.logger, userOptions.debug) + const { options: authOptions, req } = params + 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) @@ -67,7 +67,7 @@ async function AuthHandlerInternal< body: { message } as any, } } - const { pages, theme } = userOptions + const { pages, theme } = authOptions const authOnErrorPage = pages?.error && req.query?.callbackUrl?.startsWith(pages.error) @@ -93,7 +93,7 @@ async function AuthHandlerInternal< const { action, providerId, error, method = "GET" } = req const { options, cookies } = await init({ - userOptions, + authOptions, action, providerId, origin: req.origin, @@ -240,7 +240,7 @@ async function AuthHandlerInternal< } 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 cc4c77f091..cb563fbbf1 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -16,7 +16,7 @@ import type { InternalOptions } from "./types" interface InitParams { origin?: 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, origin, @@ -44,10 +44,10 @@ export async function init({ }> { const url = parseUrl(origin) - const secret = createSecret({ userOptions, url }) + const secret = createSecret({ authOptions, url }) const { providers, provider } = parseProviders({ - providers: userOptions.providers, + providers: authOptions.providers, url, providerId, }) @@ -66,7 +66,7 @@ export async function init({ buttonText: "", }, // Custom options override defaults - ...userOptions, + ...authOptions, // These computed settings can have values in userOptions but we override them // and are request-specific. url, @@ -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/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 8cbfb2b72f92b0e077e96173de7519dee23f28db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 13:12:17 +0100 Subject: [PATCH 03/26] use object for `headers` internally --- packages/next-auth/src/core/index.ts | 14 ++++------- packages/next-auth/src/core/pages/index.ts | 2 +- .../next-auth/src/core/routes/providers.ts | 2 +- packages/next-auth/src/core/routes/session.ts | 2 +- packages/next-auth/src/utils/web.ts | 23 ++++++------------- 5 files changed, 15 insertions(+), 28 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 589df84330..c2717fb7f2 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -10,6 +10,7 @@ import type { AuthAction, AuthOptions } from "./types" import type { Cookie } from "./lib/cookie" import type { ErrorType } from "./pages/error" +/** @internal */ export interface RequestInternal { /** @default "http://localhost:3000" */ origin?: string @@ -23,17 +24,12 @@ export interface RequestInternal { error?: string } -export interface NextAuthHeader { - key: string - value: string -} - -// TODO: Rename to `ResponseInternal` +/** @internal */ export interface ResponseInternal< Body extends string | Record | any[] = any > { status?: number - headers?: NextAuthHeader[] + headers?: Record body?: Body redirect?: string cookies?: Cookie[] @@ -63,7 +59,7 @@ async function AuthHandlerInternal< const message = `There is a problem with the server configuration. Check the server logs for more information.` return { status: 500, - headers: [{ key: "Content-Type", value: "application/json" }], + headers: { "Content-Type": "application/json" }, body: { message } as any, } } @@ -123,7 +119,7 @@ async function AuthHandlerInternal< } case "csrf": return { - headers: [{ key: "Content-Type", value: "application/json" }], + headers: { "Content-Type": "application/json" }, body: { csrfToken: options.csrfToken } as any, cookies, } diff --git a/packages/next-auth/src/core/pages/index.ts b/packages/next-auth/src/core/pages/index.ts index 6938a4e016..8eed148e73 100644 --- a/packages/next-auth/src/core/pages/index.ts +++ b/packages/next-auth/src/core/pages/index.ts @@ -31,7 +31,7 @@ export default function renderPage(params: RenderPageParams) { return { cookies, status, - headers: [{ key: "Content-Type", value: "text/html" }], + headers: { "Content-Type": "text/html" }, body: `${title}
${renderToString(html)}
`, diff --git a/packages/next-auth/src/core/routes/providers.ts b/packages/next-auth/src/core/routes/providers.ts index 9ce34acd44..2f6f1b0fab 100644 --- a/packages/next-auth/src/core/routes/providers.ts +++ b/packages/next-auth/src/core/routes/providers.ts @@ -18,7 +18,7 @@ export default function providers( providers: InternalProvider[] ): ResponseInternal> { return { - headers: [{ key: "Content-Type", value: "application/json" }], + headers: { "Content-Type": "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 73caccdd9b..a7eabc5113 100644 --- a/packages/next-auth/src/core/routes/session.ts +++ b/packages/next-auth/src/core/routes/session.ts @@ -31,7 +31,7 @@ export default async function session( const response: ResponseInternal = { body: {}, - headers: [{ key: "Content-Type", value: "application/json" }], + headers: { "Content-Type": "application/json" }, cookies: [], } diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index 6ce17266a2..0e8b37d72f 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -46,35 +46,26 @@ export async function toInternalRequest( ): Promise { const url = new URL(req.url) const nextauth = url.pathname.split("/").slice(3) - const headers = Object.fromEntries(req.headers) - const query: Record = Object.fromEntries(url.searchParams) - const cookieHeader = req.headers.get("cookie") ?? "" - const cookies = - parseCookie( - Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader - ) ?? {} return { action: nextauth[0] as AuthAction, method: req.method, - headers, + headers: Object.fromEntries(req.headers), body: req.body ? await readJSONBody(req.body) : undefined, - cookies: cookies, + cookies: + parseCookie( + Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader + ) ?? {}, providerId: nextauth[1], error: url.searchParams.get("error") ?? undefined, origin: url.origin, - query, + query: Object.fromEntries(url.searchParams), } } export function toResponse(res: ResponseInternal): Response { - const headers = new Headers( - res.headers?.reduce((acc, { key, value }) => { - acc[key] = value - return acc - }, {}) - ) + const headers = new Headers(res.headers) res.cookies?.forEach((cookie) => { const { name, value, options } = cookie From f9d26acf014f7d49616077e2c8cbc972e51363be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 13:13:14 +0100 Subject: [PATCH 04/26] default `method` to GET --- packages/next-auth/src/core/index.ts | 5 +++-- packages/next-auth/src/utils/web.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index c2717fb7f2..cab6f8d686 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -14,7 +14,8 @@ import type { ErrorType } from "./pages/error" export interface RequestInternal { /** @default "http://localhost:3000" */ origin?: string - method?: string + /** @default "GET" */ + method: string cookies?: Partial> headers?: Record query?: Record @@ -86,7 +87,7 @@ async function AuthHandlerInternal< } } - const { action, providerId, error, method = "GET" } = req + const { action, providerId, error, method } = req const { options, cookies } = await init({ authOptions, diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index 0e8b37d72f..5bd23d34af 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -50,7 +50,7 @@ export async function toInternalRequest( return { action: nextauth[0] as AuthAction, - method: req.method, + method: req.method ?? "GET", headers: Object.fromEntries(req.headers), body: req.body ? await readJSONBody(req.body) : undefined, cookies: From cf32136f55d8d1a4bd3e8dce95f3327b159f84d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 13:16:02 +0100 Subject: [PATCH 05/26] simplify `unstable_getServerSession` --- packages/next-auth/src/next/index.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 62f390a2f9..bf726bab34 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -139,19 +139,12 @@ export async function unstable_getServerSession< options = Object.assign({}, args[2], { providers: [] }) } - const urlOrError = getURL( - "/api/auth/session", - options.trustHost, - req.headers["x-forwarded-host"] ?? req.headers.host - ) - - if (urlOrError instanceof Error) throw urlOrError + const request = new Request("http://a/api/auth/session", { + headers: new Headers(req.headers), + }) options.secret ??= process.env.NEXTAUTH_SECRET - const response = await AuthHandler( - new Request(urlOrError, { headers: req.headers }), - options - ) + const response = await AuthHandler(request, options) const { status = 200, headers } = response From 07bccf6dcffbdf3d5e751bca21be2658b8be9e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 14:37:23 +0100 Subject: [PATCH 06/26] allow optional headers --- packages/next-auth/src/jwt/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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] From b649ca5e7e397dafbb5f9c11eaff4c76d33aa372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 14:59:28 +0100 Subject: [PATCH 07/26] revert middleware --- packages/next-auth/src/next/middleware.ts | 19 ++-- packages/next-auth/src/utils/web.ts | 13 +++ packages/next-auth/tests/middleware.test.ts | 103 ++++++++------------ 3 files changed, 61 insertions(+), 74 deletions(-) diff --git a/packages/next-auth/src/next/middleware.ts b/packages/next-auth/src/next/middleware.ts index 38ad5efaf5..99c2ea0e4c 100644 --- a/packages/next-auth/src/next/middleware.ts +++ b/packages/next-auth/src/next/middleware.ts @@ -6,7 +6,7 @@ import { NextResponse, NextRequest } from "next/server" import { getToken } from "../jwt" import parseUrl from "../utils/parse-url" -import { getURL } from "../utils/node" +import { detectHost } from "../utils/web" type AuthorizedCallback = (params: { token: JWT | null @@ -113,18 +113,19 @@ async function handleMiddleware( const signInPage = options?.pages?.signIn ?? "/api/auth/signin" const errorPage = options?.pages?.error ?? "/api/auth/error" - options.trustHost = Boolean( - options.trustHost ?? process.env.VERCEL ?? process.env.AUTH_TRUST_HOST + options.trustHost ??= !!( + process.env.NEXTAUTH_URL ?? + process.env.VERCEL ?? + process.env.AUTH_TRUST_HOST ) - let authPath - const url = getURL( - null, + const host = detectHost( options.trustHost, - req.headers.get("x-forwarded-host") ?? req.headers.get("host") + req.headers?.get("x-forwarded-host"), + process.env.NEXTAUTH_URL ?? + (process.env.NODE_ENV !== "production" && "http://localhost:3000") ) - if (url instanceof URL) authPath = parseUrl(url).path - else authPath = "/api/auth" + const authPath = parseUrl(host).path const publicPaths = ["/_next", "/favicon.ico"] diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index 5bd23d34af..55383468d8 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -93,3 +93,16 @@ export function toResponse(res: ResponseInternal): Response { return response } + +/** Extract the host from the environment */ +export function detectHost( + trusted: boolean, + forwardedValue: string | string[] | undefined | null, + defaultValue: string | false +): string | undefined { + if (trusted && forwardedValue) { + return Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue + } + + return defaultValue || undefined +} diff --git a/packages/next-auth/tests/middleware.test.ts b/packages/next-auth/tests/middleware.test.ts index 569e9301e7..b09cc4d3d3 100644 --- a/packages/next-auth/tests/middleware.test.ts +++ b/packages/next-auth/tests/middleware.test.ts @@ -6,91 +6,64 @@ it("should not match pages as public paths", async () => { pages: { signIn: "/", error: "/" }, secret: "secret", } + const handleMiddleware = withAuth(options) as NextMiddleware - const req = new NextRequest("http://127.0.0.1/protected/pathA", { - headers: { authorization: "" }, - }) + const response = await handleMiddleware( + new NextRequest("http://127.0.0.1/protected/pathA"), + null as any + ) - const handleMiddleware = withAuth(options) as NextMiddleware - const res = await handleMiddleware(req, null as any) - expect(res).toBeDefined() - expect(res?.status).toBe(307) + expect(response?.status).toBe(307) + expect(response?.headers.get("location")).toBe( + "http://localhost/?callbackUrl=%2Fprotected%2FpathA" + ) }) it("should not redirect on public paths", async () => { const options: NextAuthMiddlewareOptions = { secret: "secret" } - const req = new NextRequest("http://127.0.0.1/_next/foo", { - headers: { authorization: "" }, - }) + const req = new NextRequest("http://127.0.0.1/_next/foo") const handleMiddleware = withAuth(options) as NextMiddleware const res = await handleMiddleware(req, null as any) expect(res).toBeUndefined() }) -it("should redirect according to nextUrl basePath", async () => { - const options: NextAuthMiddlewareOptions = { secret: "secret" } - - const req = { - nextUrl: { - pathname: "/protected/pathA", - search: "", - origin: "http://127.0.0.1", - basePath: "/custom-base-path", - }, - headers: new Headers({ authorization: "" }), - } - - const handleMiddleware = withAuth(options) as NextMiddleware - const res = await handleMiddleware(req as NextRequest, 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 +it("should respect NextURL#basePath when redirecting", async () => { const options: NextAuthMiddlewareOptions = { secret: "secret" } - const handleMiddleware = withAuth(options) as NextMiddleware - const req1 = { - nextUrl: { - pathname: "/protected/pathA", - search: "", - origin: "http://127.0.0.1", - basePath: "/custom-base-path", - }, - headers: new Headers({ authorization: "" }), - } - // when - const res = await handleMiddleware(req1 as NextRequest, null as any) - - // then - expect(res).toBeDefined() - expect(res?.status).toEqual(307) - expect(res?.headers.get("location")).toContain( + 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" ) - const req2 = { - nextUrl: { - pathname: "/api/auth/signin", - search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA", - origin: "http://127.0.0.1", - basePath: "/custom-base-path", - }, - headers: new Headers({ authorization: "" }), - } - // and when follow redirect - const resFromRedirectedUrl = await handleMiddleware( - req2 as NextRequest, + // Should not redirect when invoked on sign in page + + 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 ) - // then return sign in page - expect(resFromRedirectedUrl).toBeUndefined() + expect(response2).toBeUndefined() }) From d9c7b5b741f192fcad703c236c45865f4fb9c361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 15:09:37 +0100 Subject: [PATCH 08/26] wip getURL --- packages/next-auth/src/utils/node.ts | 39 ++++++++++++++-------------- packages/next-auth/src/utils/web.ts | 1 + 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts index 341de6caaf..cf77d5e72e 100644 --- a/packages/next-auth/src/utils/node.ts +++ b/packages/next-auth/src/utils/node.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage, ServerResponse } from "http" +import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "http" import type { GetServerSidePropsContext, NextApiRequest } from "next" export function setCookie(res, value: string) { @@ -25,32 +25,31 @@ export function getBody( return { body: JSON.stringify(req.body) } } -/** Extract the host from the environment */ +/** Extract the full request URL from the environment */ export function getURL( - url: string | undefined | null, - trusted: boolean | undefined = !!( - process.env.AUTH_TRUST_HOST ?? process.env.VERCEL - ), - forwardedValue: string | string[] | undefined | null -): URL | Error { + url: string, + headers?: IncomingHttpHeaders | Headers +): URL { try { - let host = - process.env.NEXTAUTH_URL ?? - (process.env.NODE_ENV !== "production" && "http://localhost:3000") - - if (trusted && forwardedValue) { - host = Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue + let proto, host + if (headers instanceof Headers) { + proto = headers.get("x-forwarded-proto") + host = headers.get("x-forwarded-host") ?? headers.get("host") + } else { + proto = headers?.["x-forwarded-proto"] + host = headers?.["x-forwarded-host"] ?? headers?.host } - if (!host) throw new TypeError("Invalid host") - if (!url) throw new TypeError("Invalid URL, cannot determine action") + proto ??= "https" + let base = `${proto}://${host}` - if (host.startsWith("http://") || host.startsWith("https://")) { - return new URL(`${host}${url}`) + if (process.env.NODE_ENV !== "production" && !base) { + base = "http://localhost:3000" } - return new URL(`https://${host}${url}`) + + return new URL(url, base) } catch (error) { - return error as Error + return new URL("http://localhost:3000") } } diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index 55383468d8..a95e77a042 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -45,6 +45,7 @@ export async function toInternalRequest( req: Request ): Promise { const url = new URL(req.url) + // TODO: fix supporting custom basePath const nextauth = url.pathname.split("/").slice(3) const cookieHeader = req.headers.get("cookie") ?? "" From 2f7dadac5f3dffefc98af9cdec9d8750ac236b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 15:10:14 +0100 Subject: [PATCH 09/26] revert host detection --- packages/next-auth/src/core/types.ts | 2 +- packages/next-auth/src/next/index.ts | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/next-auth/src/core/types.ts b/packages/next-auth/src/core/types.ts index 4b793cda2c..e2d7fe4cdc 100644 --- a/packages/next-auth/src/core/types.ts +++ b/packages/next-auth/src/core/types.ts @@ -210,7 +210,7 @@ export interface AuthOptions { * - ⚠ **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.AUTH_TRUST_HOST ?? process.env.VERCEL) + * @default Boolean(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL) */ trustHost?: boolean } diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index bf726bab34..221cca49b6 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -1,5 +1,6 @@ import { AuthHandler } from "../core" -import { getURL, getBody, setHeaders } from "../utils/node" +import { getBody, setHeaders } from "../utils/node" +import { detectHost } from "../utils/web" import type { GetServerSidePropsContext, @@ -18,14 +19,23 @@ async function NextAuthHandler( res: NextApiResponse, options: AuthOptions ) { - const url = getURL( - req.url, - options.trustHost, - req.headers["x-forwarded-host"] ?? req.headers.host + options.trustHost ??= !!( + process.env.NEXTAUTH_URL ?? + process.env.AUTH_TRUST_HOST ?? + process.env.VERCEL ) - if (url instanceof Error) return res.status(400).end() + const { nextauth } = req.query ?? {} + const host = detectHost( + options.trustHost, + req.headers["x-forwarded-host"], + process.env.NEXTAUTH_URL ?? + (process.env.NODE_ENV !== "production" && "http://localhost:3000") + ) + const url = `${host}/${ + Array.isArray(nextauth) ? nextauth.join("/") : nextauth + }` const request = new Request(url, { headers: new Headers(req.headers as any), method: req.method, From d4d271dd4ffd5eaf6741e5ce04d15f470e6fbf53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 15:47:05 +0100 Subject: [PATCH 10/26] use old `detectHost` --- packages/next-auth/src/next/index.ts | 33 ++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 221cca49b6..bf462d7d9b 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -13,27 +13,52 @@ import type { NextAuthRequest, NextAuthResponse, } from "../core/types" +import { MissingAPIRoute } from "../core/errors" async function NextAuthHandler( req: NextApiRequest, res: NextApiResponse, options: AuthOptions ) { + const errorLogger = options.logger?.error ?? console.error + const { nextauth = [] } = req.query ?? {} + if (!nextauth.length) { + const error = new MissingAPIRoute( + "Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly." + ) + if (process.env.NODE_ENV === "production") { + errorLogger(error) + const message = `There is a problem with the server configuration. Check the server logs for more information.` + res.status(500) + return res.json({ message }) + } + throw error + } + options.trustHost ??= !!( process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL ) - - const { nextauth } = req.query ?? {} const host = detectHost( options.trustHost, req.headers["x-forwarded-host"], process.env.NEXTAUTH_URL ?? - (process.env.NODE_ENV !== "production" && "http://localhost:3000") + (process.env.NODE_ENV === "development" && "http://localhost:3000") ) - const url = `${host}/${ + if (!host) { + const error = new Error("Could not detect host.") + if (process.env.NODE_ENV === "production") { + errorLogger(error) + const message = `There is a problem with the server configuration. Check the server logs for more information.` + res.status(500) + return res.json({ message }) + } + throw error + } + + const url = `${host.replace("/api/auth", "")}/api/auth/${ Array.isArray(nextauth) ? nextauth.join("/") : nextauth }` const request = new Request(url, { From 8734abe81de2dba2ea8bde2fd6b12f9a41dd01b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 9 Dec 2022 15:47:19 +0100 Subject: [PATCH 11/26] fix/add some tests wip --- packages/next-auth/tests/next.test.ts | 59 ++++++++++++++++++--------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/next-auth/tests/next.test.ts b/packages/next-auth/tests/next.test.ts index 3cb3047d73..93bcd4e9fa 100644 --- a/packages/next-auth/tests/next.test.ts +++ b/packages/next-auth/tests/next.test.ts @@ -1,29 +1,50 @@ -// import { MissingAPIRoute } from "../src/core/errors" +import { MissingAPIRoute } from "../src/core/errors" import { nodeHandler } from "./utils" -it("Missing req.url throws MISSING_NEXTAUTH_API_ROUTE_ERROR", async () => { - const { res } = await nodeHandler() +it("Missing req.url throws in dev", async () => { + await expect(nodeHandler).rejects.toThrow(MissingAPIRoute) +}) - expect(res.status).toBeCalledWith(400) - // Moved to host detection in getUrl - // expect(logger.error).toBeCalledTimes(1) - // expect(logger.error).toBeCalledWith( - // "MISSING_NEXTAUTH_API_ROUTE_ERROR", - // expect.any(MissingAPIRoute) - // ) - // expect(res.setHeader).toBeCalledWith("content-type", "application/json") - // const body = res.send.mock.calls[0][0] - // expect(JSON.parse(body)).toEqual({ - // message: - // "There is a problem with the server configuration. Check the server logs for more information.", - // }) +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 nodeHandler() + + expect(logger.error).toBeCalledTimes(1) + const error = new MissingAPIRoute( + "Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly." + ) + expect(logger.error).toBeCalledWith(error) + + expect(res.status).toBeCalledWith(500) + expect(res.json).toBeCalledWith({ message: configErrorMessage }) + expect(logger.error).toBeCalledWith(error) + // @ts-expect-error + process.env.NODE_ENV = "test" +}) + +it("Missing host throws in dev", async () => { + await expect( + async () => + await nodeHandler({ + req: { query: { nextauth: ["session"] } }, + }) + ).rejects.toThrow(Error) }) -it("Missing host throws 400 in production", async () => { +it("Missing host config error in prod", async () => { // @ts-expect-error process.env.NODE_ENV = "production" - const { res } = await nodeHandler() - expect(res.status).toBeCalledWith(400) + const { res, logger } = await nodeHandler({ + req: { query: { nextauth: ["session"] } }, + }) + expect(res.status).toBeCalledWith(500) + expect(res.json).toBeCalledWith({ message: configErrorMessage }) + + expect(logger.error).toBeCalledWith(new Error("Could not detect host.")) // @ts-expect-error process.env.NODE_ENV = "test" }) From 1188b1030455324f049e52a44771b3726d8fb995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Dec 2022 19:02:35 +0100 Subject: [PATCH 12/26] move more to core, refactor getURL --- packages/next-auth/src/core/errors.ts | 10 ++++ packages/next-auth/src/core/index.ts | 40 ++++++++++--- packages/next-auth/src/core/lib/assert.ts | 4 +- packages/next-auth/src/next/index.ts | 70 ++++++++-------------- packages/next-auth/src/utils/node.ts | 40 ++++++------- packages/next-auth/src/utils/web.ts | 73 +++++++++++++++++------ 6 files changed, 139 insertions(+), 98 deletions(-) diff --git a/packages/next-auth/src/core/errors.ts b/packages/next-auth/src/core/errors.ts index b2eaf0ba09..1b9b91e26c 100644 --- a/packages/next-auth/src/core/errors.ts +++ b/packages/next-auth/src/core/errors.ts @@ -72,6 +72,16 @@ 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 cab6f8d686..b150118acd 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -1,19 +1,19 @@ import logger, { setLogger } from "../utils/logger" import { toInternalRequest, toResponse } from "../utils/web" -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 type { AuthAction, AuthOptions } from "./types" +import { UntrustedHost } from "./errors" import type { Cookie } from "./lib/cookie" import type { ErrorType } from "./pages/error" +import type { AuthAction, AuthOptions } from "./types" /** @internal */ export interface RequestInternal { - /** @default "http://localhost:3000" */ - origin?: string + url: URL /** @default "GET" */ method: string cookies?: Partial> @@ -36,6 +36,9 @@ export interface ResponseInternal< cookies?: Cookie[] } +const configErrorMessage = + "There is a problem with the server configuration. Check the server logs for more information." + async function AuthHandlerInternal< Body extends string | Record | any[] >(params: { @@ -45,7 +48,6 @@ async function AuthHandlerInternal< parsedBody?: any }): Promise> { const { options: authOptions, req } = params - setLogger(authOptions.logger, authOptions.debug) const assertionResult = assertConfig({ options: authOptions, req }) @@ -57,11 +59,10 @@ 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 } as any, + body: { message: configErrorMessage } as any, } } const { pages, theme } = authOptions @@ -93,7 +94,7 @@ async function AuthHandlerInternal< authOptions, action, providerId, - origin: req.origin, + origin: req.url.origin, callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl, csrfToken: req.body?.csrfToken, cookies: req.cookies, @@ -266,7 +267,28 @@ 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 }) return toResponse(internalResponse) } diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index de3088f3f4..823bba7d69 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -48,7 +48,7 @@ export function assertConfig(params: { const warnings: WarningCode[] = [] if (!warned) { - if (!req.origin) warnings.push("NEXTAUTH_URL") + if (!req.url.origin) warnings.push("NEXTAUTH_URL") // TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV` if (!options.secret && process.env.NODE_ENV !== "production") @@ -70,7 +70,7 @@ export function assertConfig(params: { const callbackUrlParam = req.query?.callbackUrl as string | undefined - const url = parseUrl(req.origin) + const url = parseUrl(req.url.origin) if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) { return new InvalidCallbackUrl( diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index bf462d7d9b..50ec43ca4f 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -1,6 +1,5 @@ import { AuthHandler } from "../core" -import { getBody, setHeaders } from "../utils/node" -import { detectHost } from "../utils/web" +import { getBody, getURL, setHeaders } from "../utils/node" import type { GetServerSidePropsContext, @@ -13,70 +12,48 @@ import type { NextAuthRequest, NextAuthResponse, } from "../core/types" -import { MissingAPIRoute } from "../core/errors" async function NextAuthHandler( req: NextApiRequest, res: NextApiResponse, options: AuthOptions ) { - const errorLogger = options.logger?.error ?? console.error - const { nextauth = [] } = req.query ?? {} - if (!nextauth.length) { - const error = new MissingAPIRoute( - "Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly." - ) - if (process.env.NODE_ENV === "production") { - errorLogger(error) - const message = `There is a problem with the server configuration. Check the server logs for more information.` - res.status(500) - return res.json({ message }) - } - throw error + 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(500) + return res.json({ + message: + "There is a problem with the server configuration. Check the server logs for more information.", + }) } - options.trustHost ??= !!( - process.env.NEXTAUTH_URL ?? - process.env.AUTH_TRUST_HOST ?? - process.env.VERCEL - ) - const host = detectHost( - options.trustHost, - req.headers["x-forwarded-host"], - process.env.NEXTAUTH_URL ?? - (process.env.NODE_ENV === "development" && "http://localhost:3000") - ) - - if (!host) { - const error = new Error("Could not detect host.") - if (process.env.NODE_ENV === "production") { - errorLogger(error) - const message = `There is a problem with the server configuration. Check the server logs for more information.` - res.status(500) - return res.json({ message }) - } - throw error - } - - const url = `${host.replace("/api/auth", "")}/api/auth/${ - Array.isArray(nextauth) ? nextauth.join("/") : nextauth - }` const request = new Request(url, { - headers: new Headers(req.headers as any), + headers, method: req.method, ...getBody(req), }) 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" + ) + const response = await AuthHandler(request, options) - const { status, headers } = response + const { status } = response res.status(status) - setHeaders(headers, res) + setHeaders(response.headers, res) // If the request expects a return URL, send it as JSON // instead of doing an actual redirect. - const redirect = headers.get("Location") + const redirect = response.headers.get("Location") if (req.body?.json === "true" && redirect) { res.removeHeader("Location") @@ -179,6 +156,7 @@ export async function unstable_getServerSession< }) options.secret ??= process.env.NEXTAUTH_SECRET + options.trustHost = true const response = await AuthHandler(request, options) const { status = 200, headers } = response diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts index cf77d5e72e..c5bfa6e4b5 100644 --- a/packages/next-auth/src/utils/node.ts +++ b/packages/next-auth/src/utils/node.ts @@ -1,4 +1,4 @@ -import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "http" +import type { IncomingMessage, ServerResponse } from "http" import type { GetServerSidePropsContext, NextApiRequest } from "next" export function setCookie(res, value: string) { @@ -26,30 +26,26 @@ export function getBody( } /** Extract the full request URL from the environment */ -export function getURL( - url: string, - headers?: IncomingHttpHeaders | Headers -): URL { +export function getURL(url: string | undefined, headers: Headers): URL | Error { try { - let proto, host - if (headers instanceof Headers) { - proto = headers.get("x-forwarded-proto") - host = headers.get("x-forwarded-host") ?? headers.get("host") - } else { - proto = headers?.["x-forwarded-proto"] - host = headers?.["x-forwarded-host"] ?? headers?.host - } - - proto ??= "https" - let base = `${proto}://${host}` - - if (process.env.NODE_ENV !== "production" && !base) { - base = "http://localhost:3000" + if (!url) throw new Error("Missing url") + if (process.env.NEXTAUTH_URL) { + const base = process.env.NEXTAUTH_URL + const segments = base.split("/").filter(Boolean) + const hasCustomPath = segments.join("/").endsWith("api/auth") + if (hasCustomPath) { + const custom = segments.slice(0, -2).join("/") + // FIXME: path seems to have duplicate segments + return new URL(custom + url, base) + } + return new URL(url, base) } - - return new URL(url, base) + const proto = headers.get("x-forwarded-proto") ?? "https" + const host = headers.get("x-forwarded-host") ?? headers.get("host") + const origin = `${proto}://${host}` + return new URL(url, origin) } catch (error) { - return new URL("http://localhost:3000") + return error as Error } } diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index a95e77a042..5b921f7a41 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -1,4 +1,5 @@ import { serialize, parse as parseCookie } from "cookie" +import { UnknownAction } from "../core/errors" import type { ResponseInternal, RequestInternal } from "../core" import type { AuthAction } from "../core/types" @@ -41,27 +42,60 @@ async function readJSONBody( } } +const actions = [ + "providers", + "session", + "csrf", + "signin", + "signout", + "callback", + "verify-request", + "error", + "_log", +] + export async function toInternalRequest( req: Request -): Promise { - const url = new URL(req.url) - // TODO: fix supporting custom basePath - const nextauth = url.pathname.split("/").slice(3) - const cookieHeader = req.headers.get("cookie") ?? "" - - return { - action: nextauth[0] as AuthAction, - method: req.method ?? "GET", - headers: Object.fromEntries(req.headers), - body: req.body ? await readJSONBody(req.body) : undefined, - cookies: - parseCookie( - Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader - ) ?? {}, - providerId: nextauth[1], - error: url.searchParams.get("error") ?? undefined, - origin: url.origin, - query: Object.fromEntries(url.searchParams), +): Promise { + try { + const url = new URL(req.url.replace(/\/$/, "")) + const { pathname } = url + + const action = actions.find((a) => pathname.includes(a)) as + | AuthAction + | undefined + 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 + } + + const cookieHeader = req.headers.get("cookie") ?? "" + + return { + url, + action, + providerId, + method: req.method ?? "GET", + headers: Object.fromEntries(req.headers), + body: req.body ? await readJSONBody(req.body) : undefined, + cookies: + parseCookie( + Array.isArray(cookieHeader) ? cookieHeader.join(";") : cookieHeader + ) ?? {}, + error: url.searchParams.get("error") ?? undefined, + query: Object.fromEntries(url.searchParams), + } + } catch (error) { + return error } } @@ -95,6 +129,7 @@ export function toResponse(res: ResponseInternal): Response { return response } +// TODO: Remove /** Extract the host from the environment */ export function detectHost( trusted: boolean, From 254f03ab6f5dc41e3d13b593cadce7d93d10fd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Dec 2022 19:23:23 +0100 Subject: [PATCH 13/26] better type auth actions --- packages/next-auth/src/utils/web.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index 5b921f7a41..d0a968a4dc 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -42,17 +42,8 @@ async function readJSONBody( } } -const actions = [ - "providers", - "session", - "csrf", - "signin", - "signout", - "callback", - "verify-request", - "error", - "_log", -] +// prettier-ignore +const actions: AuthAction[] = [ "providers", "session", "csrf", "signin", "signout", "callback", "verify-request", "error", "_log" ] export async function toInternalRequest( req: Request @@ -61,9 +52,7 @@ export async function toInternalRequest( const url = new URL(req.url.replace(/\/$/, "")) const { pathname } = url - const action = actions.find((a) => pathname.includes(a)) as - | AuthAction - | undefined + const action = actions.find((a) => pathname.includes(a)) if (!action) { throw new UnknownAction("Cannot detect action.") } From ba782de410f69f5a22a8ca6cd1518d919cc29a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 12:48:51 +0100 Subject: [PATCH 14/26] fix custom path support (w/ api/auth) --- packages/next-auth/src/utils/node.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts index c5bfa6e4b5..356774e320 100644 --- a/packages/next-auth/src/utils/node.ts +++ b/packages/next-auth/src/utils/node.ts @@ -25,24 +25,32 @@ export function getBody( return { body: JSON.stringify(req.body) } } -/** Extract the full request URL from the environment */ +/** + * 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 = process.env.NEXTAUTH_URL - const segments = base.split("/").filter(Boolean) + const base = new URL(process.env.NEXTAUTH_URL) + if (!["http:", "https:"].includes(base.protocol)) { + throw new Error("Invalid protocol") + } + const segments = base.pathname.split("/").filter(Boolean) + // TODO: Support custom path without /api/auth const hasCustomPath = segments.join("/").endsWith("api/auth") if (hasCustomPath) { const custom = segments.slice(0, -2).join("/") - // FIXME: path seems to have duplicate segments - return new URL(custom + url, base) + return new URL("/" + custom + url, base.origin) } return new URL(url, base) } const proto = headers.get("x-forwarded-proto") ?? "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 From 3323774a2df03bcbc6f7267178af63f9963a7fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 12:48:58 +0100 Subject: [PATCH 15/26] add `getURL` tests --- packages/next-auth/tests/getURL.test.ts | 122 ++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 packages/next-auth/tests/getURL.test.ts diff --git a/packages/next-auth/tests/getURL.test.ts b/packages/next-auth/tests/getURL.test.ts new file mode 100644 index 0000000000..52803fd609 --- /dev/null +++ b/packages/next-auth/tests/getURL.test.ts @@ -0,0 +1,122 @@ +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: https:///") + ) +}) + +it("Should read host headers", () => { + expect(getURL("/api/auth/session", { host: "localhost" })).toBeURL( + // also checks that default protocol is https + "https://localhost/api/auth/session" + ) + + expect( + getURL("/custom/api/auth/session", { "x-forwarded-host": "localhost:3000" }) + ).toBeURL("https://localhost:3000/custom/api/auth/session") + + // Prefer x-forwarded-host over host + expect( + getURL("/", { host: "localhost", "x-forwarded-host": "localhost:3000" }) + ).toBeURL("https://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" + ) + + // 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" + ) + + // Different than /api/auth + process.env.NEXTAUTH_URL = "http://localhost:3000/custom/nextauth" + expect(getURL("/nextauth/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 + } + } +} From 67502cdee31284fcb78b67a48a2c208d9e91b4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 12:51:04 +0100 Subject: [PATCH 16/26] fix email tests --- packages/next-auth/tests/email.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/next-auth/tests/email.test.ts b/packages/next-auth/tests/email.test.ts index c98356f147..55d5590cc9 100644 --- a/packages/next-auth/tests/email.test.ts +++ b/packages/next-auth/tests/email.test.ts @@ -14,6 +14,7 @@ it("Send e-mail to the only address correctly", async () => { providers: [EmailProvider({ sendVerificationRequest })], callbacks: { signIn }, secret, + trustHost: true, }, { path: "signin/email", @@ -54,6 +55,7 @@ it("Send e-mail to first address only", async () => { providers: [EmailProvider({ sendVerificationRequest })], callbacks: { signIn }, secret, + trustHost: true, }, { path: "signin/email", @@ -94,6 +96,7 @@ it("Send e-mail to address with first domain", async () => { providers: [EmailProvider({ sendVerificationRequest })], callbacks: { signIn }, secret, + trustHost: true, }, { path: "signin/email", @@ -140,6 +143,7 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => { }), ], secret, + trustHost: true, }, { path: "signin/email", From c51c266280a1023cf4095230ef1a1fb9f37d9029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 12:52:55 +0100 Subject: [PATCH 17/26] fix assert tests --- packages/next-auth/tests/assert.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/next-auth/tests/assert.test.ts b/packages/next-auth/tests/assert.test.ts index 33e5680079..3b8570c27b 100644 --- a/packages/next-auth/tests/assert.test.ts +++ b/packages/next-auth/tests/assert.test.ts @@ -9,7 +9,7 @@ import EmailProvider from "../src/providers/email" it("Show error page if secret is not defined", async () => { const { res, log } = await handler( - { providers: [], secret: undefined }, + { providers: [], secret: undefined, trustHost: true }, { prod: true } ) @@ -28,6 +28,7 @@ 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 } ) @@ -48,6 +49,7 @@ it("Show error page if adapter is not configured when using with email", async ( { providers: [EmailProvider({ sendVerificationRequest })], secret: "secret", + trustHost: true, }, { prod: true } ) @@ -64,7 +66,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: [] }, + { providers: [], trustHost: true }, { prod: true, params: { callbackUrl: "invalid-callback" } } ) @@ -80,7 +82,7 @@ it("Should show configuration error page on invalid `callbackUrl`", async () => it("Allow relative `callbackUrl`", async () => { const { res, log } = await handler( - { providers: [] }, + { providers: [], trustHost: true }, { prod: true, params: { callbackUrl: "/callback" } } ) From 5b5d97bf5bc7be01702406e31546871c6be41339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 13:14:50 +0100 Subject: [PATCH 18/26] custom base without api/auth, with trailing slash --- packages/next-auth/src/utils/node.ts | 12 +++++++----- packages/next-auth/tests/getURL.test.ts | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts index 356774e320..6858d6c9d3 100644 --- a/packages/next-auth/src/utils/node.ts +++ b/packages/next-auth/src/utils/node.ts @@ -37,12 +37,14 @@ export function getURL(url: string | undefined, headers: Headers): URL | Error { if (!["http:", "https:"].includes(base.protocol)) { throw new Error("Invalid protocol") } - const segments = base.pathname.split("/").filter(Boolean) - // TODO: Support custom path without /api/auth - const hasCustomPath = segments.join("/").endsWith("api/auth") + const hasCustomPath = base.pathname !== "/" + if (hasCustomPath) { - const custom = segments.slice(0, -2).join("/") - return new URL("/" + custom + url, base.origin) + 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) } diff --git a/packages/next-auth/tests/getURL.test.ts b/packages/next-auth/tests/getURL.test.ts index 52803fd609..306aa98e87 100644 --- a/packages/next-auth/tests/getURL.test.ts +++ b/packages/next-auth/tests/getURL.test.ts @@ -70,15 +70,32 @@ describe("process.env.NEXTAUTH_URL", () => { "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" ) - // Different than /api/auth + 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("/nextauth/session", {})).toBeURL( + 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" ) }) From d2c2273457cba080b10a7a2e60898b0d6b133cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 13:41:56 +0100 Subject: [PATCH 19/26] remove parseUrl from assert.ts --- packages/next-auth/src/core/lib/assert.ts | 11 ++++------- packages/next-auth/src/utils/parse-url.ts | 5 ++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index 823bba7d69..0859619568 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -7,7 +7,6 @@ import { InvalidCallbackUrl, MissingAdapterMethods, } from "../errors" -import parseUrl from "../../utils/parse-url" import { defaultCookies } from "./cookie" import type { RequestInternal } from ".." @@ -44,7 +43,7 @@ export function assertConfig(params: { req: RequestInternal }): ConfigError | WarningCode[] { const { options, req } = params - + const { url } = req const warnings: WarningCode[] = [] if (!warned) { @@ -70,21 +69,19 @@ export function assertConfig(params: { const callbackUrlParam = req.query?.callbackUrl as string | undefined - const url = parseUrl(req.url.origin) - - if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) { + if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.origin)) { return new InvalidCallbackUrl( `Invalid callback URL. Received: ${callbackUrlParam}` ) } const { callbackUrl: defaultCallbackUrl } = defaultCookies( - options.useSecureCookies ?? url.base.startsWith("https://") + options.useSecureCookies ?? url.protocol === "https://" ) const callbackUrlCookie = req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name] - if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.base)) { + if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.origin)) { return new InvalidCallbackUrl( `Invalid callback URL. Received: ${callbackUrlCookie}` ) diff --git a/packages/next-auth/src/utils/parse-url.ts b/packages/next-auth/src/utils/parse-url.ts index 7f63b0ada2..49add525b3 100644 --- a/packages/next-auth/src/utils/parse-url.ts +++ b/packages/next-auth/src/utils/parse-url.ts @@ -11,7 +11,10 @@ export interface InternalUrl { toString: () => string } -/** Returns an `URL` like object to make requests/redirects from server-side */ +/** + * 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 { const defaultUrl = new URL("http://localhost:3000/api/auth") From 09d28b02b24e6b6e33b28f4d452ae24ff34ce5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 13:42:09 +0100 Subject: [PATCH 20/26] return 400 when wrong url --- packages/next-auth/src/next/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 50ec43ca4f..6ceeb76d60 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -24,7 +24,7 @@ async function NextAuthHandler( if (process.env.NODE_ENV !== "production") throw url const errorLogger = options.logger?.error ?? console.error errorLogger("INVALID_URL", url) - res.status(500) + res.status(400) return res.json({ message: "There is a problem with the server configuration. Check the server logs for more information.", From 0af22a5fe4da31777c19c21799c73b5474b76cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 13:45:47 +0100 Subject: [PATCH 21/26] fix tests --- packages/next-auth/tests/next.test.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/next-auth/tests/next.test.ts b/packages/next-auth/tests/next.test.ts index 93bcd4e9fa..d2ccd792b4 100644 --- a/packages/next-auth/tests/next.test.ts +++ b/packages/next-auth/tests/next.test.ts @@ -1,8 +1,7 @@ -import { MissingAPIRoute } from "../src/core/errors" import { nodeHandler } from "./utils" it("Missing req.url throws in dev", async () => { - await expect(nodeHandler).rejects.toThrow(MissingAPIRoute) + await expect(nodeHandler).rejects.toThrow(new Error("Missing url")) }) const configErrorMessage = @@ -14,14 +13,12 @@ it("Missing req.url returns config error in prod", async () => { const { res, logger } = await nodeHandler() expect(logger.error).toBeCalledTimes(1) - const error = new MissingAPIRoute( - "Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly." - ) - expect(logger.error).toBeCalledWith(error) + const error = new Error("Missing url") + expect(logger.error).toBeCalledWith("INVALID_URL", error) - expect(res.status).toBeCalledWith(500) + expect(res.status).toBeCalledWith(400) expect(res.json).toBeCalledWith({ message: configErrorMessage }) - expect(logger.error).toBeCalledWith(error) + // @ts-expect-error process.env.NODE_ENV = "test" }) @@ -41,10 +38,10 @@ it("Missing host config error in prod", async () => { const { res, logger } = await nodeHandler({ req: { query: { nextauth: ["session"] } }, }) - expect(res.status).toBeCalledWith(500) + expect(res.status).toBeCalledWith(400) expect(res.json).toBeCalledWith({ message: configErrorMessage }) - expect(logger.error).toBeCalledWith(new Error("Could not detect host.")) + expect(logger.error).toBeCalledWith("INVALID_URL", new Error("Missing url")) // @ts-expect-error process.env.NODE_ENV = "test" }) From d67e0e23ee399fac0ab229fc319d63e7a0fcaacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 14:05:31 +0100 Subject: [PATCH 22/26] refactor --- packages/next-auth/src/core/lib/assert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index 0859619568..589a025d82 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -47,7 +47,7 @@ export function assertConfig(params: { const warnings: WarningCode[] = [] if (!warned) { - if (!req.url.origin) warnings.push("NEXTAUTH_URL") + if (!url.origin) warnings.push("NEXTAUTH_URL") // TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV` if (!options.secret && process.env.NODE_ENV !== "production") From d350132b09880083c82c834a5e2cccd7f04649a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 14:44:55 +0100 Subject: [PATCH 23/26] fix protocol in dev --- packages/next-auth/src/next/index.ts | 16 +++++++++++++--- packages/next-auth/src/utils/node.ts | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/next-auth/src/next/index.ts b/packages/next-auth/src/next/index.ts index 32a588abf1..7697892fad 100644 --- a/packages/next-auth/src/next/index.ts +++ b/packages/next-auth/src/next/index.ts @@ -140,9 +140,19 @@ export async function unstable_getServerSession< options = Object.assign({}, args[2], { providers: [] }) } - const request = new Request("http://a/api/auth/session", { - headers: new Headers(req.headers), - }) + 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 diff --git a/packages/next-auth/src/utils/node.ts b/packages/next-auth/src/utils/node.ts index 6858d6c9d3..0219ea08a7 100644 --- a/packages/next-auth/src/utils/node.ts +++ b/packages/next-auth/src/utils/node.ts @@ -48,7 +48,9 @@ export function getURL(url: string | undefined, headers: Headers): URL | Error { } return new URL(url, base) } - const proto = headers.get("x-forwarded-proto") ?? "https" + 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}` From 64832a0a36b4f6526fd756a50d8bd497bf2e8c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 14:45:01 +0100 Subject: [PATCH 24/26] fix tests --- packages/next-auth/tests/getURL.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/next-auth/tests/getURL.test.ts b/packages/next-auth/tests/getURL.test.ts index 306aa98e87..61f24f4f3e 100644 --- a/packages/next-auth/tests/getURL.test.ts +++ b/packages/next-auth/tests/getURL.test.ts @@ -16,24 +16,23 @@ it("Should return error when invalid protocol", () => { it("Should return error when invalid host", () => { expect(getURL("/", { host: "/" })).toEqual( - new TypeError("Invalid base URL: https:///") + new TypeError("Invalid base URL: http:///") ) }) it("Should read host headers", () => { expect(getURL("/api/auth/session", { host: "localhost" })).toBeURL( - // also checks that default protocol is https - "https://localhost/api/auth/session" + "http://localhost/api/auth/session" ) expect( getURL("/custom/api/auth/session", { "x-forwarded-host": "localhost:3000" }) - ).toBeURL("https://localhost:3000/custom/api/auth/session") + ).toBeURL("http://localhost:3000/custom/api/auth/session") // Prefer x-forwarded-host over host expect( getURL("/", { host: "localhost", "x-forwarded-host": "localhost:3000" }) - ).toBeURL("https://localhost:3000/") + ).toBeURL("http://localhost:3000/") }) it("Should read protocol headers", () => { From 6017bdd43ed923ea7ed66f7db506b16894ef4df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 15:40:31 +0100 Subject: [PATCH 25/26] fix custom url handling --- packages/next-auth/src/core/index.ts | 2 +- packages/next-auth/src/core/init.ts | 14 +++++++++----- packages/next-auth/src/core/lib/providers.ts | 3 +-- packages/next-auth/src/core/lib/utils.ts | 6 +----- packages/next-auth/src/core/pages/error.tsx | 3 +-- packages/next-auth/src/core/pages/signout.tsx | 3 +-- packages/next-auth/src/core/types.ts | 8 +------- 7 files changed, 15 insertions(+), 24 deletions(-) diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index eb19b0e440..9b7e4c09a0 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -94,7 +94,7 @@ async function AuthHandlerInternal< authOptions, action, providerId, - origin: req.url.origin, + url: req.url, callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl, csrfToken: req.body?.csrfToken, cookies: req.cookies, diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index cb563fbbf1..f931c2f213 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,9 +12,10 @@ import { createCallbackUrl } from "./lib/callback-url" import { RequestInternal } from "." import type { InternalOptions } from "./types" +import parseUrl from "../utils/parse-url" interface InitParams { - origin?: string + url: URL authOptions: AuthOptions providerId?: string action: InternalOptions["action"] @@ -33,7 +33,7 @@ export async function init({ authOptions, providerId, action, - origin, + url: reqUrl, cookies: reqCookies, callbackUrl: reqCallbackUrl, csrfToken: reqCsrfToken, @@ -42,7 +42,11 @@ export async function init({ options: InternalOptions cookies: cookie.Cookie[] }> { - const url = parseUrl(origin) + const parsed = parseUrl( + reqUrl.origin + + reqUrl.pathname.replace(`/${action}`, "").replace(`/${providerId}`, "") + ) + const url = new URL(parsed.toString()) const secret = createSecret({ authOptions, url }) @@ -75,7 +79,7 @@ export async function init({ provider, cookies: { ...cookie.defaultCookies( - authOptions.useSecureCookies ?? url.base.startsWith("https://") + authOptions.useSecureCookies ?? url.protocol === "https:" ), // Allow user cookie options to override any cookie settings above ...authOptions.cookies, diff --git a/packages/next-auth/src/core/lib/providers.ts b/packages/next-auth/src/core/lib/providers.ts index 892bd374f8..5f350add7f 100644 --- a/packages/next-auth/src/core/lib/providers.ts +++ b/packages/next-auth/src/core/lib/providers.ts @@ -6,7 +6,6 @@ import type { OAuthConfig, Provider, } from "../../providers" -import type { InternalUrl } from "../../utils/parse-url" /** * Adds `signinUrl` and `callbackUrl` to each provider @@ -14,7 +13,7 @@ import type { InternalUrl } from "../../utils/parse-url" */ export default function parseProviders(params: { providers: Provider[] - url: InternalUrl + url: URL providerId?: string }): { providers: InternalProvider[] diff --git a/packages/next-auth/src/core/lib/utils.ts b/packages/next-auth/src/core/lib/utils.ts index c8296a766b..c2ed6991c5 100644 --- a/packages/next-auth/src/core/lib/utils.ts +++ b/packages/next-auth/src/core/lib/utils.ts @@ -2,7 +2,6 @@ import { createHash } from "crypto" import type { AuthOptions } from "../.." import type { InternalOptions } from "../types" -import type { InternalUrl } from "../../utils/parse-url" /** * Takes a number in seconds and returns the date in the future. @@ -28,10 +27,7 @@ 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: InternalUrl -}) { +export function createSecret(params: { authOptions: AuthOptions; url: URL }) { const { authOptions, url } = params return ( diff --git a/packages/next-auth/src/core/pages/error.tsx b/packages/next-auth/src/core/pages/error.tsx index e3f5562e57..b2b803b38f 100644 --- a/packages/next-auth/src/core/pages/error.tsx +++ b/packages/next-auth/src/core/pages/error.tsx @@ -1,5 +1,4 @@ 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. @@ -12,7 +11,7 @@ export type ErrorType = | "verification" export interface ErrorProps { - url?: InternalUrl + url?: URL theme?: Theme error?: ErrorType } diff --git a/packages/next-auth/src/core/pages/signout.tsx b/packages/next-auth/src/core/pages/signout.tsx index 352d825753..3d986a1040 100644 --- a/packages/next-auth/src/core/pages/signout.tsx +++ b/packages/next-auth/src/core/pages/signout.tsx @@ -1,8 +1,7 @@ import { Theme } from "../.." -import { InternalUrl } from "../../utils/parse-url" export interface SignoutProps { - url: InternalUrl + url: URL csrfToken: string theme: Theme } diff --git a/packages/next-auth/src/core/types.ts b/packages/next-auth/src/core/types.ts index e2d7fe4cdc..11137b7a2b 100644 --- a/packages/next-auth/src/core/types.ts +++ b/packages/next-auth/src/core/types.ts @@ -14,8 +14,6 @@ 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 } @@ -528,11 +526,7 @@ export interface InternalOptions< WithVerificationToken = TProviderType extends "email" ? true : false > { providers: InternalProvider[] - /** - * Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel. - * @default "http://localhost:3000/api/auth" - */ - url: InternalUrl + url: URL action: AuthAction provider: InternalProvider csrfToken?: string From 35848e68144b416135e5308ec2935de64ddc6973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 11 Dec 2022 15:41:46 +0100 Subject: [PATCH 26/26] add todo comments --- packages/next-auth/src/core/init.ts | 1 + packages/next-auth/src/utils/web.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/next-auth/src/core/init.ts b/packages/next-auth/src/core/init.ts index f931c2f213..ede030a56e 100644 --- a/packages/next-auth/src/core/init.ts +++ b/packages/next-auth/src/core/init.ts @@ -42,6 +42,7 @@ 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}`, "") diff --git a/packages/next-auth/src/utils/web.ts b/packages/next-auth/src/utils/web.ts index d0a968a4dc..1c0e9aac9d 100644 --- a/packages/next-auth/src/utils/web.ts +++ b/packages/next-auth/src/utils/web.ts @@ -49,6 +49,8 @@ export async function toInternalRequest( req: Request ): Promise { try { + // TODO: .toString() should not inclide action and providerId + // see init.ts const url = new URL(req.url.replace(/\/$/, "")) const { pathname } = url