diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 7f672cef8..7da61beb0 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -52,12 +52,16 @@ export declare function getSessionForSSR( hasToken: boolean; error: Error | undefined; }>; -export declare function withSession( - request: Request, - handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, +export declare function withSession< + RequestType extends ParsableRequest = Request, + ResponseType extends Response = Response +>( + request: RequestType, + handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, options?: VerifySessionOptions, - userContext?: Record -): Promise; + userContext?: Record, + getCookieFn?: GetCookieFn +): Promise; export declare function addCookies( baseResponse: CollectingResponse, userResponse: UserResponseType diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index e5e63fa76..e83316624 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -194,9 +194,9 @@ async function getSessionForSSR(request, jwks) { } } exports.getSessionForSSR = getSessionForSSR; -async function withSession(request, handler, options, userContext) { +async function withSession(request, handler, options, userContext, getCookieFn = getCookieFromRequest) { try { - const baseRequest = createPreParsedRequest(request); + const baseRequest = createPreParsedRequest(request, getCookieFn); const { session, response, baseResponse } = await getSessionDetails(baseRequest, options, userContext); if (response !== undefined) { return response; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 777538c84..019006d3d 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -26,7 +26,6 @@ var __rest = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.withPreParsedRequestResponse = exports.withSession = exports.getSSRSession = exports.getAppDirRequestHandler = exports.superTokensNextWrapper = void 0; -const cookie_1 = require("cookie"); const express_1 = require("./framework/express"); const utils_1 = require("./utils"); const custom_1 = require("./framework/custom"); @@ -77,13 +76,14 @@ class NextJS { return customFramework_1.getHandleCall(NextResponse, stMiddleware); } static async getSSRSession(cookies, headers, options, userContext) { - let cookiesObj = Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value])); + // Create an instance of PreParsedRequest without access to the actual + // request body and inject cookies into it. let baseRequest = new custom_1.PreParsedRequest({ method: "get", url: "", query: {}, headers: headers, - cookies: cookiesObj, + cookies: Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value])), getFormBody: async () => [], getJSONBody: async () => [], }); @@ -93,70 +93,8 @@ class NextJS { return result; } static async withSession(req, handler, options, userContext) { - try { - const query = Object.fromEntries(new URL(req.url).searchParams.entries()); - const cookies = Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); - let baseRequest = new custom_1.PreParsedRequest({ - method: req.method, - url: req.url, - query: query, - headers: req.headers, - cookies: cookies, - getFormBody: () => req.formData(), - getJSONBody: () => req.json(), - }); - const { session, response, baseResponse } = await customFramework_1.getSessionDetails( - baseRequest, - options, - utils_1.getUserContext(userContext) - ); - if (response) { - return response; - } - let userResponse; - try { - userResponse = await handler(undefined, session); - } catch (err) { - userResponse = await customFramework_1.handleError(err, baseRequest, baseResponse); - } - let didAddCookies = false; - let didAddHeaders = false; - for (const respCookie of baseResponse.cookies) { - didAddCookies = true; - userResponse.headers.append( - "Set-Cookie", - cookie_1.serialize(respCookie.key, respCookie.value, { - domain: respCookie.domain, - expires: new Date(respCookie.expires), - httpOnly: respCookie.httpOnly, - path: respCookie.path, - sameSite: respCookie.sameSite, - secure: respCookie.secure, - }) - ); - } - baseResponse.headers.forEach((value, key) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - /** - * For some deployment services (Vercel for example) production builds can return cached results for - * APIs with older header values. In this case if the session tokens have changed (because of refreshing - * for example) the cached result would still contain the older tokens and sessions would stop working. - * - * As a result, if we add cookies or headers from base response we also set the Cache-Control header - * to make sure that the final result is not a cached version. - */ - if (didAddCookies || didAddHeaders) { - if (!userResponse.headers.has("Cache-Control")) { - // This is needed for production deployments with Vercel - userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); - } - } - return userResponse; - } catch (error) { - return await handler(error, undefined); - } + const getCookieFromNextReq = NextJS.getCookieExtractor(); + return await customFramework_1.withSession(req, handler, options, userContext, getCookieFromNextReq); } static async withPreParsedRequestResponse(req, handler) { const getCookieFromNextReq = NextJS.getCookieExtractor(); diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 1d0e9660a..e32d840de 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -228,18 +228,22 @@ export async function getSessionForSSR( } } -export async function withSession( - request: Request, - handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, +export async function withSession< + RequestType extends ParsableRequest = Request, + ResponseType extends Response = Response +>( + request: RequestType, + handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, options?: VerifySessionOptions, - userContext?: Record -): Promise { + userContext?: Record, + getCookieFn: GetCookieFn = getCookieFromRequest +): Promise { try { - const baseRequest = createPreParsedRequest(request); + const baseRequest = createPreParsedRequest(request, getCookieFn); const { session, response, baseResponse } = await getSessionDetails(baseRequest, options, userContext); if (response !== undefined) { - return response; + return response as ResponseType; } let userResponse: Response; @@ -250,7 +254,7 @@ export async function withSession( userResponse = await handleError(err, baseRequest, baseResponse); } - return addCookies(baseResponse, userResponse); + return addCookies(baseResponse, userResponse) as ResponseType; } catch (error) { return await handler(error as Error, undefined); } diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index 2e71148c3..38fe2d3ca 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -13,11 +13,9 @@ * under the License. */ -import { serialize } from "cookie"; import { errorHandler } from "./framework/express"; import { getUserContext } from "./utils"; import { CollectingResponse, PreParsedRequest, middleware } from "./framework/custom"; -import { HTTPMethod } from "./types"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { addCookies, @@ -26,6 +24,7 @@ import { getHandleCall, getSessionDetails, handleError, + withSession as customWithSession, } from "./customFramework"; function next( @@ -96,11 +95,9 @@ export default class NextJS { static getAppDirRequestHandler(NextResponse: typeof Response) { const getCookieFromNextReq = NextJS.getCookieExtractor(); - const stMiddleware = middleware((req) => { return createPreParsedRequest(req, getCookieFromNextReq); }); - return getHandleCall(NextResponse, stMiddleware); } @@ -114,16 +111,14 @@ export default class NextJS { hasToken: boolean; hasInvalidClaims: boolean; }> { - let cookiesObj: Record = Object.fromEntries( - cookies.map((cookie) => [cookie.name, cookie.value]) - ); - + // Create an instance of PreParsedRequest without access to the actual + // request body and inject cookies into it. let baseRequest = new PreParsedRequest({ method: "get", url: "", query: {}, headers: headers, - cookies: cookiesObj, + cookies: Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value])), getFormBody: async () => [], getJSONBody: async () => [], }); @@ -142,82 +137,14 @@ export default class NextJS { options?: VerifySessionOptions, userContext?: Record ): Promise { - try { - const query = Object.fromEntries(new URL(req.url).searchParams.entries()); - const cookies: Record = Object.fromEntries( - req.cookies.getAll().map((cookie) => [cookie.name, cookie.value]) - ); - - let baseRequest = new PreParsedRequest({ - method: req.method as HTTPMethod, - url: req.url, - query: query, - headers: req.headers, - cookies: cookies, - getFormBody: () => req!.formData(), - getJSONBody: () => req!.json(), - }); - - const { session, response, baseResponse } = await getSessionDetails( - baseRequest, - options, - getUserContext(userContext) - ); - - if (response) { - return response as NextResponse; - } - - let userResponse: NextResponse; - - try { - userResponse = await handler(undefined, session); - } catch (err) { - userResponse = await handleError(err, baseRequest, baseResponse); - } - - let didAddCookies = false; - let didAddHeaders = false; - - for (const respCookie of baseResponse.cookies) { - didAddCookies = true; - userResponse.headers.append( - "Set-Cookie", - serialize(respCookie.key, respCookie.value, { - domain: respCookie.domain, - expires: new Date(respCookie.expires), - httpOnly: respCookie.httpOnly, - path: respCookie.path, - sameSite: respCookie.sameSite, - secure: respCookie.secure, - }) - ); - } - - baseResponse.headers.forEach((value: string, key: string) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - - /** - * For some deployment services (Vercel for example) production builds can return cached results for - * APIs with older header values. In this case if the session tokens have changed (because of refreshing - * for example) the cached result would still contain the older tokens and sessions would stop working. - * - * As a result, if we add cookies or headers from base response we also set the Cache-Control header - * to make sure that the final result is not a cached version. - */ - if (didAddCookies || didAddHeaders) { - if (!userResponse.headers.has("Cache-Control")) { - // This is needed for production deployments with Vercel - userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); - } - } - - return userResponse; - } catch (error) { - return await handler(error as Error, undefined); - } + const getCookieFromNextReq = NextJS.getCookieExtractor(); + return await customWithSession( + req, + handler, + options, + userContext, + getCookieFromNextReq + ); } static async withPreParsedRequestResponse( @@ -226,7 +153,6 @@ export default class NextJS { ): Promise { const getCookieFromNextReq = NextJS.getCookieExtractor(); let baseRequest = createPreParsedRequest(req, getCookieFromNextReq); - let baseResponse = new CollectingResponse(); let userResponse: NextResponse;