From 8830a2566171ca539df66c70c376704f9bd869ad Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 2 Oct 2024 09:56:23 +0530 Subject: [PATCH 01/28] Add init util methods for custom framework support --- lib/build/customFramework.d.ts | 9 +++++ lib/build/customFramework.js | 59 ++++++++++++++++++++++++++++++++ lib/ts/customFramework.ts | 62 ++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 lib/build/customFramework.d.ts create mode 100644 lib/build/customFramework.js create mode 100644 lib/ts/customFramework.ts diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts new file mode 100644 index 000000000..6165f63a8 --- /dev/null +++ b/lib/build/customFramework.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +/** + * This file has definition of various re-usable util methods + * that can be used to easily integrate the SDK with most + * frameworks if they are not directly supported. + */ +import { PreParsedRequest } from "./framework/custom"; +export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; +export declare function createPreParsedRequest(request: Request): PreParsedRequest; diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js new file mode 100644 index 000000000..42a9ba1b9 --- /dev/null +++ b/lib/build/customFramework.js @@ -0,0 +1,59 @@ +"use strict"; +/** + * This file has definition of various re-usable util methods + * that can be used to easily integrate the SDK with most + * frameworks if they are not directly supported. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createPreParsedRequest = void 0; +const custom_1 = require("./framework/custom"); +function createPreParsedRequest(request) { + /** + * This helper function can take any `Request` type of object + * and parse the details into an equivalent PreParsedRequest + * that can be used with the custom framework helpers. + */ + return new custom_1.PreParsedRequest({ + cookies: getCookieFromRequest(request), + url: request.url, + method: request.method, + query: getQueryFromRequest(request), + headers: request.headers, + getFormBody: async () => { + return await request.formData(); + }, + getJSONBody: async () => { + return await request.json(); + }, + }); +} +exports.createPreParsedRequest = createPreParsedRequest; +function getCookieFromRequest(request) { + /** + * This function will extract the cookies from any `Request` + * type of object and return them to be usable with PreParsedRequest. + */ + const cookies = {}; + const cookieHeader = request.headers.get("Cookie"); + if (cookieHeader) { + const cookieStrings = cookieHeader.split(";"); + for (const cookieString of cookieStrings) { + const [name, value] = cookieString.trim().split("="); + cookies[name] = decodeURIComponent(value); + } + } + return cookies; +} +function getQueryFromRequest(request) { + /** + * Helper function to extract query from any `Request` type of + * object and return them to be usable with PreParsedRequest. + */ + const query = {}; + const url = new URL(request.url); + const searchParams = url.searchParams; + searchParams.forEach((value, key) => { + query[key] = value; + }); + return query; +} diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts new file mode 100644 index 000000000..237f41371 --- /dev/null +++ b/lib/ts/customFramework.ts @@ -0,0 +1,62 @@ +/** + * This file has definition of various re-usable util methods + * that can be used to easily integrate the SDK with most + * frameworks if they are not directly supported. + */ + +import { PreParsedRequest } from "./framework/custom"; + +// Define supported types for HTTPMethod +export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; + +export function createPreParsedRequest(request: Request): PreParsedRequest { + /** + * This helper function can take any `Request` type of object + * and parse the details into an equivalent PreParsedRequest + * that can be used with the custom framework helpers. + */ + return new PreParsedRequest({ + cookies: getCookieFromRequest(request), + url: request.url as string, + method: request.method as HTTPMethod, + query: getQueryFromRequest(request), + headers: request.headers, + getFormBody: async () => { + return await request.formData(); + }, + getJSONBody: async () => { + return await request.json(); + }, + }); +} + +function getCookieFromRequest(request: Request): Record { + /** + * This function will extract the cookies from any `Request` + * type of object and return them to be usable with PreParsedRequest. + */ + const cookies: Record = {}; + const cookieHeader = request.headers.get("Cookie"); + if (cookieHeader) { + const cookieStrings = cookieHeader.split(";"); + for (const cookieString of cookieStrings) { + const [name, value] = cookieString.trim().split("="); + cookies[name] = decodeURIComponent(value); + } + } + return cookies; +} + +function getQueryFromRequest(request: Request): Record { + /** + * Helper function to extract query from any `Request` type of + * object and return them to be usable with PreParsedRequest. + */ + const query: Record = {}; + const url = new URL(request.url); + const searchParams = url.searchParams; + searchParams.forEach((value, key) => { + query[key] = value; + }); + return query; +} From daa0cb6071d8bb7da94fe2fd3ec7effb19422863 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 2 Oct 2024 10:42:01 +0530 Subject: [PATCH 02/28] Add some more changes for the custom framework re-usability --- lib/ts/customFramework.ts | 232 +++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 1 deletion(-) diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 237f41371..91a8260bc 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -4,7 +4,13 @@ * frameworks if they are not directly supported. */ -import { PreParsedRequest } from "./framework/custom"; +import { serialize } from "cookie"; +import { CollectingResponse, errorHandler, middleware, PreParsedRequest } from "./framework/custom"; +import Session, { SessionContainer, VerifySessionOptions } from "./recipe/session"; +import SessionRecipe from "./recipe/session/recipe"; +import { availableTokenTransferMethods } from "./recipe/session/constants"; +import { getToken } from "./recipe/session/cookieAndHeaders"; +import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; // Define supported types for HTTPMethod export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; @@ -60,3 +66,227 @@ function getQueryFromRequest(request: Request): Record { }); return query; } + +function getAccessToken(request: Request): string | undefined { + return getCookieFromRequest(request)["sAccessToken"]; +} + +function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { + client.getSigningKey(header.kid, (err, key) => { + if (err) { + callback(err); + } else { + const signingKey = key?.getPublicKey(); + callback(null, signingKey); + } + }); +} + +async function verifyToken(token: string): Promise { + return new Promise((resolve, reject) => { + JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { + if (err) { + reject(err); + } else { + resolve(decoded as JwtPayload); + } + }); + }); +} + +export function handleAuthAPIRequest(CustomResponse: typeof Response) { + /** + * Util function to handle all calls by intercepting them, calling + * Supertokens middleware and then accordingly returning. + */ + const stMiddleware = middleware((req) => { + return createPreParsedRequest(req); + }); + + return async function handleCall(req: Request) { + const baseResponse = new CollectingResponse(); + + const { handled, error } = await stMiddleware(req, baseResponse); + + if (error) { + throw error; + } + if (!handled) { + return new CustomResponse("Not found", { status: 404 }); + } + + for (const respCookie of baseResponse.cookies) { + baseResponse.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, + }) + ); + } + + return new CustomResponse(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); + }; +} + + +async function getSessionDetails( + preParsedRequest: PreParsedRequest, + options?: VerifySessionOptions, + userContext?: Record +): Promise<{ + session: SessionContainer | undefined; + hasToken: boolean; + hasInvalidClaims: boolean; + baseResponse: CollectingResponse; + RemixResponse?: Response; +}> { + const baseResponse = new CollectingResponse(); + // Possible interop issue. + const recipe = (SessionRecipe as any).default.instance; + const tokenTransferMethod = recipe.config.getTokenTransferMethod({ + req: preParsedRequest, + forCreateNewSession: false, + userContext, + }); + const transferMethods = tokenTransferMethod === "any" ? availableTokenTransferMethods : [tokenTransferMethod]; + const hasToken = transferMethods.some((transferMethod) => { + const token = getToken(preParsedRequest, "access", transferMethod); + if (!token) { + return false; + } + try { + parseJWTWithoutSignatureVerification(token); + return true; + } catch { + return false; + } + }); + + try { + const session = await Session.getSession(preParsedRequest, baseResponse, options, userContext); + return { + session, + hasInvalidClaims: false, + hasToken, + baseResponse, + }; + } catch (err) { + if (Session.Error.isErrorFromSuperTokens(err)) { + return { + hasToken, + hasInvalidClaims: err.type === Session.Error.INVALID_CLAIMS, + session: undefined, + baseResponse, + RemixResponse: new Response("Authentication required", { + status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401, + }), + }; + } else { + throw err; + } + } +} + + +/** + * A helper function to retrieve session details on the server side. + * + * NOTE: This function does not use the getSession function from the supertokens-node SDK + * because getSession can update the access token. These updated tokens would not be + * propagated to the client side, as request interceptors do not run on the server side. + */ +export async function getSessionForSSR(remixRequest: Request): Promise<{ + accessTokenPayload: JwtPayload | undefined; + hasToken: boolean; + error: Error | undefined; +}> { + const accessToken = getAccessToken(remixRequest); + const hasToken = !!accessToken; + try { + if (accessToken) { + const decoded = await verifyToken(accessToken); + return { accessTokenPayload: decoded, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } catch (error) { + if (error instanceof JsonWebToken.TokenExpiredError) { + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: error as Error }; + } +} + +export async function withSession( + remixRequest: Request, + handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, + options?: VerifySessionOptions, + userContext?: Record +): Promise { + try { + const baseRequest = createPreParsedRequest(remixRequest); + const { session, RemixResponse, baseResponse } = await getSessionDetails(baseRequest, options, userContext); + + if (RemixResponse !== undefined) { + return RemixResponse; + } + + let userResponse: Response; + + try { + userResponse = await handler(undefined, session); + } catch (err) { + await errorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { + if (errorHandlerError) { + throw errorHandlerError; + } + }); + + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + userResponse = new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }); + } + + 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); + }); + 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); + } +} From 6bd73193daf0f28292695f7330a3bc2a1e9aa85e Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 2 Oct 2024 16:56:43 +0530 Subject: [PATCH 03/28] Expose custom framework --- custom/index.d.ts | 11 ++ custom/index.js | 6 ++ lib/build/customFramework.d.ts | 24 +++++ lib/build/customFramework.js | 184 ++++++++++++++++++++++++++++++++- lib/ts/customFramework.ts | 41 +++----- package.json | 12 +++ 6 files changed, 249 insertions(+), 29 deletions(-) create mode 100644 custom/index.d.ts create mode 100644 custom/index.js diff --git a/custom/index.d.ts b/custom/index.d.ts new file mode 100644 index 000000000..6abf6c029 --- /dev/null +++ b/custom/index.d.ts @@ -0,0 +1,11 @@ +export * from "../lib/build/customFramework"; +/** + * 'export *' does not re-export a default. + * import CustomFramework from "supertokens-node/custom"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ + +import * as _default from "../lib/build/customFramework"; +export default _default; diff --git a/custom/index.js b/custom/index.js new file mode 100644 index 000000000..ba833dc37 --- /dev/null +++ b/custom/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../lib/build/customFramework")); diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 6165f63a8..4699f2d68 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -5,5 +5,29 @@ * frameworks if they are not directly supported. */ import { PreParsedRequest } from "./framework/custom"; +import { SessionContainer, VerifySessionOptions } from "./recipe/session"; +import { JWTPayload } from "jose"; export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; export declare function createPreParsedRequest(request: Request): PreParsedRequest; +export declare function handleAuthAPIRequest(CustomResponse: typeof Response): (req: Request) => Promise; +/** + * A helper function to retrieve session details on the server side. + * + * NOTE: This function does not use the getSession function from the supertokens-node SDK + * because getSession can update the access token. These updated tokens would not be + * propagated to the client side, as request interceptors do not run on the server side. + */ +export declare function getSessionForSSR( + request: Request, + jwks: any +): Promise<{ + accessTokenPayload: JWTPayload | undefined; + hasToken: boolean; + error: Error | undefined; +}>; +export declare function withSession( + remixRequest: Request, + handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, + options?: VerifySessionOptions, + userContext?: Record +): Promise; diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 42a9ba1b9..368098b49 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -4,9 +4,21 @@ * that can be used to easily integrate the SDK with most * frameworks if they are not directly supported. */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createPreParsedRequest = void 0; +exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.createPreParsedRequest = void 0; +const cookie_1 = require("cookie"); const custom_1 = require("./framework/custom"); +const session_1 = __importDefault(require("./recipe/session")); +const recipe_1 = __importDefault(require("./recipe/session/recipe")); +const constants_1 = require("./recipe/session/constants"); +const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); +const jwt_1 = require("./recipe/session/jwt"); +const jose_1 = require("jose"); function createPreParsedRequest(request) { /** * This helper function can take any `Request` type of object @@ -57,3 +69,173 @@ function getQueryFromRequest(request) { }); return query; } +function getAccessToken(request) { + return getCookieFromRequest(request)["sAccessToken"]; +} +async function verifyToken(token, jwks) { + // Verify the JWT using the remote JWK set and return the payload + const { payload } = await jose_1.jwtVerify(token, jwks); + return payload; +} +function handleAuthAPIRequest(CustomResponse) { + /** + * Util function to handle all calls by intercepting them, calling + * Supertokens middleware and then accordingly returning. + */ + const stMiddleware = custom_1.middleware((req) => { + return createPreParsedRequest(req); + }); + return async function handleCall(req) { + const baseResponse = new custom_1.CollectingResponse(); + const { handled, error } = await stMiddleware(req, baseResponse); + if (error) { + throw error; + } + if (!handled) { + return new CustomResponse("Not found", { status: 404 }); + } + for (const respCookie of baseResponse.cookies) { + baseResponse.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, + }) + ); + } + return new CustomResponse(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); + }; +} +exports.handleAuthAPIRequest = handleAuthAPIRequest; +async function getSessionDetails(preParsedRequest, options, userContext) { + const baseResponse = new custom_1.CollectingResponse(); + // Possible interop issue. + const recipe = recipe_1.default.default.instance; + const tokenTransferMethod = recipe.config.getTokenTransferMethod({ + req: preParsedRequest, + forCreateNewSession: false, + userContext, + }); + const transferMethods = + tokenTransferMethod === "any" ? constants_1.availableTokenTransferMethods : [tokenTransferMethod]; + const hasToken = transferMethods.some((transferMethod) => { + const token = cookieAndHeaders_1.getToken(preParsedRequest, "access", transferMethod); + if (!token) { + return false; + } + try { + jwt_1.parseJWTWithoutSignatureVerification(token); + return true; + } catch (_a) { + return false; + } + }); + try { + const session = await session_1.default.getSession(preParsedRequest, baseResponse, options, userContext); + return { + session, + hasInvalidClaims: false, + hasToken, + baseResponse, + }; + } catch (err) { + if (session_1.default.Error.isErrorFromSuperTokens(err)) { + return { + hasToken, + hasInvalidClaims: err.type === session_1.default.Error.INVALID_CLAIMS, + session: undefined, + baseResponse, + RemixResponse: new Response("Authentication required", { + status: err.type === session_1.default.Error.INVALID_CLAIMS ? 403 : 401, + }), + }; + } else { + throw err; + } + } +} +/** + * A helper function to retrieve session details on the server side. + * + * NOTE: This function does not use the getSession function from the supertokens-node SDK + * because getSession can update the access token. These updated tokens would not be + * propagated to the client side, as request interceptors do not run on the server side. + */ +async function getSessionForSSR(request, jwks) { + const accessToken = getAccessToken(request); + const hasToken = !!accessToken; + try { + if (accessToken) { + const decoded = await verifyToken(accessToken, jwks); + return { accessTokenPayload: decoded, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } catch (error) { + if (error instanceof Error && error.name === "JWTExpired") { + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: error }; + } +} +exports.getSessionForSSR = getSessionForSSR; +async function withSession(remixRequest, handler, options, userContext) { + try { + const baseRequest = createPreParsedRequest(remixRequest); + const { session, RemixResponse, baseResponse } = await getSessionDetails(baseRequest, options, userContext); + if (RemixResponse !== undefined) { + return RemixResponse; + } + let userResponse; + try { + userResponse = await handler(undefined, session); + } catch (err) { + await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { + if (errorHandlerError) { + throw errorHandlerError; + } + }); + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + userResponse = new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }); + } + 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); + }); + 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); + } +} +exports.withSession = withSession; diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 91a8260bc..10cc730a3 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -11,6 +11,7 @@ import SessionRecipe from "./recipe/session/recipe"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; +import { jwtVerify, JWTPayload } from "jose"; // Define supported types for HTTPMethod export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; @@ -71,27 +72,10 @@ function getAccessToken(request: Request): string | undefined { return getCookieFromRequest(request)["sAccessToken"]; } -function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, (err, key) => { - if (err) { - callback(err); - } else { - const signingKey = key?.getPublicKey(); - callback(null, signingKey); - } - }); -} - -async function verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as JwtPayload); - } - }); - }); +async function verifyToken(token: string, jwks: any): Promise { + // Verify the JWT using the remote JWK set and return the payload + const { payload } = await jwtVerify(token, jwks); + return payload; } export function handleAuthAPIRequest(CustomResponse: typeof Response) { @@ -136,7 +120,6 @@ export function handleAuthAPIRequest(CustomResponse: typeof Response) { }; } - async function getSessionDetails( preParsedRequest: PreParsedRequest, options?: VerifySessionOptions, @@ -195,7 +178,6 @@ async function getSessionDetails( } } - /** * A helper function to retrieve session details on the server side. * @@ -203,21 +185,24 @@ async function getSessionDetails( * because getSession can update the access token. These updated tokens would not be * propagated to the client side, as request interceptors do not run on the server side. */ -export async function getSessionForSSR(remixRequest: Request): Promise<{ - accessTokenPayload: JwtPayload | undefined; +export async function getSessionForSSR( + request: Request, + jwks: any +): Promise<{ + accessTokenPayload: JWTPayload | undefined; hasToken: boolean; error: Error | undefined; }> { - const accessToken = getAccessToken(remixRequest); + const accessToken = getAccessToken(request); const hasToken = !!accessToken; try { if (accessToken) { - const decoded = await verifyToken(accessToken); + const decoded = await verifyToken(accessToken, jwks); return { accessTokenPayload: decoded, hasToken, error: undefined }; } return { accessTokenPayload: undefined, hasToken, error: undefined }; } catch (error) { - if (error instanceof JsonWebToken.TokenExpiredError) { + if (error instanceof Error && error.name === "JWTExpired") { return { accessTokenPayload: undefined, hasToken, error: undefined }; } return { accessTokenPayload: undefined, hasToken, error: error as Error }; diff --git a/package.json b/package.json index c1bddbf95..c3ba37f46 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,18 @@ "types": "./nextjs/index.d.ts", "default": "./nextjs/index.js" }, + "./customFramework": { + "types": "./custom/index.d.ts", + "default": "./custom/index.js" + }, + "./customFramework/index": { + "types": "./custom/index.d.ts", + "default": "./custom/index.js" + }, + "./customFramework/index.js": { + "types": "./custom/index.d.ts", + "default": "./custom/index.js" + }, "./types": { "types": "./types/index.d.ts", "default": "./types/index.js" From 0d4506fb8555977add0bed7294410424f2cd4b1e Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 2 Oct 2024 16:58:44 +0530 Subject: [PATCH 04/28] Update changelog and bump version --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b92c113..aab4cd538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a way to run CI on unmerged PRs - Added support for FDIs: 3.1 and 4.0. Required by: auth-react >=0.49.0 and web-js>=0.15.0 - The `networkInterceptor` now also gets a new `params` prop in the request config. +- Adds `customFramework` util functions to minimize code required in custom frameworks like remix, astro etc. ### Breaking change From 433d041519c84574530bdd86454f0d0812811de7 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Wed, 2 Oct 2024 19:31:52 +0530 Subject: [PATCH 05/28] Add a fix for accessing SessionRecipe --- lib/build/customFramework.js | 4 ++-- lib/ts/customFramework.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 368098b49..975ea5171 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -117,11 +117,11 @@ exports.handleAuthAPIRequest = handleAuthAPIRequest; async function getSessionDetails(preParsedRequest, options, userContext) { const baseResponse = new custom_1.CollectingResponse(); // Possible interop issue. - const recipe = recipe_1.default.default.instance; + const recipe = recipe_1.default.getInstanceOrThrowError(); const tokenTransferMethod = recipe.config.getTokenTransferMethod({ req: preParsedRequest, forCreateNewSession: false, - userContext, + userContext: userContext, }); const transferMethods = tokenTransferMethod === "any" ? constants_1.availableTokenTransferMethods : [tokenTransferMethod]; diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 10cc730a3..c0015957c 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -133,11 +133,11 @@ async function getSessionDetails( }> { const baseResponse = new CollectingResponse(); // Possible interop issue. - const recipe = (SessionRecipe as any).default.instance; + const recipe = SessionRecipe.getInstanceOrThrowError(); const tokenTransferMethod = recipe.config.getTokenTransferMethod({ req: preParsedRequest, forCreateNewSession: false, - userContext, + userContext: userContext as any, }); const transferMethods = tokenTransferMethod === "any" ? availableTokenTransferMethods : [tokenTransferMethod]; const hasToken = transferMethods.some((transferMethod) => { From f80df58a282fe74fe99abd75fc487106abbfb9b7 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 11:10:03 +0530 Subject: [PATCH 06/28] Add init tests for the custom framework implementation --- test/customFramework.test.js | 182 +++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 test/customFramework.test.js diff --git a/test/customFramework.test.js b/test/customFramework.test.js new file mode 100644 index 000000000..5d02a8b14 --- /dev/null +++ b/test/customFramework.test.js @@ -0,0 +1,182 @@ +let assert = require("assert"); +const { createPreParsedRequest, handleAuthAPIRequest } = require("../lib/build/customFramework"); +let { ProcessState } = require("../lib/build/processState"); +let SuperTokens = require("../lib/build/").default; +const Session = require("../lib/build/recipe/session"); +const EmailPassword = require("../lib/build/recipe/emailpassword"); +const { PreParsedRequest } = require("../lib/build/framework/custom"); +const { printPath, setupST, startST, killAllST, cleanST, delay } = require("./utils"); + +describe(`createPreParsedRequest ${printPath("[test/customFramework.test.js]")}`, () => { + it("should create a PreParsedRequest with correct properties from the Request object", async () => { + // Mock a Request object + const mockRequest = { + url: "https://example.com/path?name=test", + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + Authorization: "Bearer token", + Cookie: "session=abcd1234; theme=dark", + }), + formData: async () => new FormData(), + json: async () => ({ key: "value" }), + }; + + // Assume getCookieFromRequest and getQueryFromRequest return specific mock data + const mockCookies = { session: "abcd1234", theme: "dark" }; + const mockQuery = { name: "test" }; + + // Create the PreParsedRequest + const preParsedReq = createPreParsedRequest(mockRequest); + + // Assertions + assert(preParsedReq instanceof PreParsedRequest, "Should return an instance of PreParsedRequest"); + assert.deepStrictEqual( + preParsedReq.getCookieValue("session"), + mockCookies.session, + "Should parse `session` value from cookie correctly" + ); + assert.deepStrictEqual( + preParsedReq.getCookieValue("theme"), + mockCookies.theme, + "Should parse `session` value from cookie correctly" + ); + assert.strictEqual(preParsedReq.getOriginalURL(), mockRequest.url, "Should set the correct URL"); + assert.strictEqual(preParsedReq.getMethod(), mockRequest.method.toLowerCase(), "Should set the correct method"); + assert.deepStrictEqual( + preParsedReq.getKeyValueFromQuery("name"), + mockQuery.name, + "Should parse query parameters correctly" + ); + assert.strictEqual( + preParsedReq.getHeaderValue("Authorization"), + mockRequest.headers.get("Authorization"), + "Should set the correct headers" + ); + + // Test getJSONBody methods + const jsonBody = await preParsedReq.getJSONBody(); + + assert.deepStrictEqual(jsonBody, { key: "value" }, "getJSONBody should return parsed JSON body"); + }); +}); + +describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, () => { + let connectionURI; + + before(async function () { + process.env.user = undefined; + await killAllST(); + await setupST(); + connectionURI = await startST(); + ProcessState.getInstance().reset(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + apiBasePath: "/api/auth", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init({ + override: { + functions: (oI) => { + return { + ...oI, + createNewSession: async (input) => { + let session = await oI.createNewSession(input); + process.env.user = session.getUserId(); + return session; + }, + }; + }, + }, + }), + ], + }); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + const CustomResponse = class extends Response {}; + + it("should sign-up successfully", async () => { + const handleCall = handleAuthAPIRequest(CustomResponse); + + const mockRequest = new Request(`${connectionURI}/api/auth/signup/`, { + method: "POST", + headers: { + rid: "emailpassword", + }, + body: JSON.stringify({ + formFields: [ + { + id: "email", + value: "john.doe@supertokens.io", + }, + { + id: "password", + value: "P@sSW0rd", + }, + ], + }), + }); + + // Call handleCall + const response = await handleCall(mockRequest); + + // Assertions for response + assert.strictEqual(response.status, 200, "Should return status 200"); + const responseBody = await response.json(); + assert.strictEqual(responseBody.status, "OK", "Response status should be OK"); + assert.ok(response.headers.get("st-access-token"), "st-access-token header should be set"); + assert.ok(response.headers.get("st-refresh-token"), "st-refresh-token header should be set"); + assert.ok(response.headers.get("front-token"), "front-token header should be set"); + }); + + it("should sign-in successfully", async () => { + const handleCall = handleAuthAPIRequest(CustomResponse); + + const mockRequest = new Request(`${connectionURI}/api/auth/signin/`, { + method: "POST", + headers: { + rid: "emailpassword", + }, + body: JSON.stringify({ + formFields: [ + { + id: "email", + value: "john.doe@supertokens.io", + }, + { + id: "password", + value: "P@sSW0rd", + }, + ], + }), + }); + + // Call handleCall + const response = await handleCall(mockRequest); + + // Assertions for response + assert.strictEqual(response.status, 200, "Should return status 200"); + const responseBody = await response.json(); + assert.strictEqual(responseBody.status, "OK", "Response status should be OK"); + assert.deepStrictEqual( + responseBody.user.emails[0], + "john.doe@supertokens.io", + "User email should be returned correctly" + ); + assert.ok(response.headers.get("st-access-token"), "st-access-token header should be set"); + assert.ok(response.headers.get("st-refresh-token"), "st-refresh-token header should be set"); + assert.ok(response.headers.get("front-token"), "front-token header should be set"); + }); +}); From 61c50531481596479b6dd771cd98804772ed8f7b Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 11:13:06 +0530 Subject: [PATCH 07/28] Update some variable names to match the custom framework definition --- lib/build/customFramework.d.ts | 2 +- lib/build/customFramework.js | 12 ++++++------ lib/ts/customFramework.ts | 14 +++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 4699f2d68..eec048b93 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -26,7 +26,7 @@ export declare function getSessionForSSR( error: Error | undefined; }>; export declare function withSession( - remixRequest: Request, + request: Request, handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, options?: VerifySessionOptions, userContext?: Record diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 975ea5171..f6d54ad38 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -152,7 +152,7 @@ async function getSessionDetails(preParsedRequest, options, userContext) { hasInvalidClaims: err.type === session_1.default.Error.INVALID_CLAIMS, session: undefined, baseResponse, - RemixResponse: new Response("Authentication required", { + response: new Response("Authentication required", { status: err.type === session_1.default.Error.INVALID_CLAIMS ? 403 : 401, }), }; @@ -185,12 +185,12 @@ async function getSessionForSSR(request, jwks) { } } exports.getSessionForSSR = getSessionForSSR; -async function withSession(remixRequest, handler, options, userContext) { +async function withSession(request, handler, options, userContext) { try { - const baseRequest = createPreParsedRequest(remixRequest); - const { session, RemixResponse, baseResponse } = await getSessionDetails(baseRequest, options, userContext); - if (RemixResponse !== undefined) { - return RemixResponse; + const baseRequest = createPreParsedRequest(request); + const { session, response, baseResponse } = await getSessionDetails(baseRequest, options, userContext); + if (response !== undefined) { + return response; } let userResponse; try { diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index c0015957c..9d0ff28bb 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -129,7 +129,7 @@ async function getSessionDetails( hasToken: boolean; hasInvalidClaims: boolean; baseResponse: CollectingResponse; - RemixResponse?: Response; + response?: Response; }> { const baseResponse = new CollectingResponse(); // Possible interop issue. @@ -168,7 +168,7 @@ async function getSessionDetails( hasInvalidClaims: err.type === Session.Error.INVALID_CLAIMS, session: undefined, baseResponse, - RemixResponse: new Response("Authentication required", { + response: new Response("Authentication required", { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401, }), }; @@ -210,17 +210,17 @@ export async function getSessionForSSR( } export async function withSession( - remixRequest: Request, + request: Request, handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, options?: VerifySessionOptions, userContext?: Record ): Promise { try { - const baseRequest = createPreParsedRequest(remixRequest); - const { session, RemixResponse, baseResponse } = await getSessionDetails(baseRequest, options, userContext); + const baseRequest = createPreParsedRequest(request); + const { session, response, baseResponse } = await getSessionDetails(baseRequest, options, userContext); - if (RemixResponse !== undefined) { - return RemixResponse; + if (response !== undefined) { + return response; } let userResponse: Response; From 57078ac3d25efbe003f7a5d3dd3bdfa20964d5f5 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 11:44:12 +0530 Subject: [PATCH 08/28] Add more tests for custom framework regarding withSession --- test/customFramework.test.js | 109 ++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/test/customFramework.test.js b/test/customFramework.test.js index 5d02a8b14..59e51985c 100644 --- a/test/customFramework.test.js +++ b/test/customFramework.test.js @@ -1,5 +1,5 @@ let assert = require("assert"); -const { createPreParsedRequest, handleAuthAPIRequest } = require("../lib/build/customFramework"); +const { createPreParsedRequest, handleAuthAPIRequest, withSession } = require("../lib/build/customFramework"); let { ProcessState } = require("../lib/build/processState"); let SuperTokens = require("../lib/build/").default; const Session = require("../lib/build/recipe/session"); @@ -63,6 +63,7 @@ describe(`createPreParsedRequest ${printPath("[test/customFramework.test.js]")}` describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, () => { let connectionURI; + let accessToken; before(async function () { process.env.user = undefined; @@ -175,8 +176,112 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, "john.doe@supertokens.io", "User email should be returned correctly" ); - assert.ok(response.headers.get("st-access-token"), "st-access-token header should be set"); + + accessToken = response.headers.get("st-access-token"); + + assert.ok(accessToken, "st-access-token header should be set"); assert.ok(response.headers.get("st-refresh-token"), "st-refresh-token header should be set"); assert.ok(response.headers.get("front-token"), "front-token header should be set"); }); + + // Case 1: Successful => add session to request object. + it("withSession should create a session properly", async () => { + const mockSessionRequest = new Request(`${connectionURI}/api/user/`, { + method: "POST", + headers: { + rid: "emailpassword", + }, + headers: { + authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + formFields: [ + { + id: "email", + value: "john.doe@supertokens.io", + }, + { + id: "password", + value: "P@sSW0rd", + }, + ], + }), + }); + + const sessionResponse = await withSession(mockSessionRequest, async (err, session) => { + assert.strictEqual(err, undefined, "Error should be undefined"); + assert.ok(session, "Session should be present"); + assert.strictEqual(session.getUserId(), process.env.user, "Session user ID should match"); + + // Return success response + return new Response( + JSON.stringify({ + status: "session created", + userId: session.getUserId(), + }), + { status: 200 } + ); + }); + + // Assertions for the response + assert.strictEqual(sessionResponse.status, 200, "Should return status 200"); + const sessionResponseBody = await sessionResponse.json(); + assert.strictEqual( + sessionResponseBody.status, + "session created", + "Response status should be 'session created'" + ); + assert.strictEqual( + sessionResponseBody.userId, + process.env.user, + "Response user ID should match session user ID" + ); + }); + + // Case 2: Error => throws error when no access token is passed. + it("withSession should pass error when session fails", async () => { + const mockSessionRequest = new Request(`${connectionURI}/api/user/`, { + method: "POST", + headers: { + rid: "emailpassword", + }, + headers: {}, + body: JSON.stringify({ + formFields: [ + { + id: "email", + value: "john.doe@supertokens.io", + }, + { + id: "password", + value: "P@sSW0rd", + }, + ], + }), + }); + + const sessionResponse = await withSession(mockSessionRequest, async (err, session) => { + // No action required since the function will throw an error due to unauthorized + }); + + // Assertions for the response + assert.strictEqual(sessionResponse.status, 401, "Should return status 401"); + }); + + it("should return 404 for unhandled routes", async () => { + const handleCall = handleAuthAPIRequest(CustomResponse); + + const mockRequest = new Request(`${connectionURI}/api/auth/test/`, { + method: "GET", + headers: { + rid: "emailpassword", + }, + }); + + // Call handleCall + const response = await handleCall(mockRequest); + + assert.strictEqual(response.status, 404, "Should return status 404"); + assert.strictEqual(await response.text(), "Not found", "Should return Not found"); + }); }); From c015bad73aba32b008fe2cbdac8f56348bd61d4c Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 12:15:29 +0530 Subject: [PATCH 09/28] Add tests for getSessionForSSR --- .mocharc.yml | 2 +- package.json | 2 +- test/customFramework.test.js | 113 ++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/.mocharc.yml b/.mocharc.yml index 509f8ef3b..cb61a123d 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -1,4 +1,4 @@ -spec: test/**/*.test.js +# spec: test/**/*.test.js exit: true slow: 10000 timeout: 20000 diff --git a/package.json b/package.json index c3ba37f46..4962ba606 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { - "test": "TEST_MODE=testing npx mocha --node-option no-experimental-fetch -r test/fetch-polyfill.mjs --timeout 500000", + "test": "TEST_MODE=testing npx mocha --bail --node-option no-experimental-fetch -r test/fetch-polyfill.mjs --timeout 500000", "test-exports": "./test/testExports.sh", "build-check": "cd lib && npx tsc -p tsconfig.json --noEmit && cd ../test/with-typescript && npm run build", "build": "cd lib && rm -rf build && npx tsc -p tsconfig.json && cd ../test/with-typescript && npm run build && cd ../.. && npm run post-build", diff --git a/test/customFramework.test.js b/test/customFramework.test.js index 59e51985c..c010b0da4 100644 --- a/test/customFramework.test.js +++ b/test/customFramework.test.js @@ -1,11 +1,32 @@ let assert = require("assert"); -const { createPreParsedRequest, handleAuthAPIRequest, withSession } = require("../lib/build/customFramework"); +const { + createPreParsedRequest, + handleAuthAPIRequest, + withSession, + getSessionForSSR, +} = require("../lib/build/customFramework"); let { ProcessState } = require("../lib/build/processState"); let SuperTokens = require("../lib/build/").default; const Session = require("../lib/build/recipe/session"); const EmailPassword = require("../lib/build/recipe/emailpassword"); const { PreParsedRequest } = require("../lib/build/framework/custom"); const { printPath, setupST, startST, killAllST, cleanST, delay } = require("./utils"); +const { generateKeyPair, SignJWT } = require("jose"); + +// Helper function to create a JWKS +async function createJWKS() { + const { privateKey } = await generateKeyPair("RS256"); + return privateKey; +} + +// Function to sign a JWT +async function signJWT(privateKey, payload, expiresIn = "2h") { + return new SignJWT(payload) + .setProtectedHeader({ alg: "RS256", kid: "test-key-id" }) + .setIssuedAt() + .setExpirationTime(expiresIn) + .sign(privateKey); +} describe(`createPreParsedRequest ${printPath("[test/customFramework.test.js]")}`, () => { it("should create a PreParsedRequest with correct properties from the Request object", async () => { @@ -64,6 +85,7 @@ describe(`createPreParsedRequest ${printPath("[test/customFramework.test.js]")}` describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, () => { let connectionURI; let accessToken; + let privateKey; before(async function () { process.env.user = undefined; @@ -99,6 +121,8 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, }), ], }); + + privateKey = await createJWKS(); }); after(async function () { @@ -284,4 +308,91 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, assert.strictEqual(response.status, 404, "Should return status 404"); assert.strictEqual(await response.text(), "Not found", "Should return Not found"); }); + + it("getSessionForSSR should return session for valid token", async () => { + // Create a valid JWT payload + const payload = { userId: "123", email: "john.doe@example.com" }; + + // Sign the JWT + const validToken = await signJWT(privateKey, payload); + + // Create a mock request containing the valid token as a cookie + const mockRequest = new Request("https://example.com", { + headers: { Cookie: `sAccessToken=${validToken}` }, + }); + + // Call the getSessionForSSR function + const result = await getSessionForSSR(mockRequest, privateKey); + + // Assertions + assert.strictEqual(result.hasToken, true, "hasToken should be true for a valid token"); + assert.ok(result.accessTokenPayload, "accessTokenPayload should be present for a valid token"); + assert.strictEqual(result.error, undefined, "error should be undefined for a valid token"); + assert.strictEqual(result.accessTokenPayload.userId, "123", "User ID in payload should match"); + assert.strictEqual(result.accessTokenPayload.email, "john.doe@example.com", "Email in payload should match"); + }); + + it("should return undefined accessTokenPayload and hasToken as false when no token is present", async () => { + // Create a request without an access token + const mockRequest = new Request("https://example.com"); + + // Call the getSessionForSSR function + const result = await getSessionForSSR(mockRequest, privateKey); + + // Assertions + assert.strictEqual(result.hasToken, false, "hasToken should be false when no token is present"); + assert.strictEqual( + result.accessTokenPayload, + undefined, + "accessTokenPayload should be undefined when no token is present" + ); + assert.strictEqual(result.error, undefined, "error should be undefined when no token is present"); + }); + + it("should handle an expired token gracefully", async () => { + // Create a payload for the token + const payload = { userId: "123", email: "john.doe@example.com" }; + + // Sign the JWT with an expiration time in the past (e.g., 1 second ago) + const expiredToken = await signJWT(privateKey, payload, Math.floor(Date.now() / 1000) - 1); + + // Create a mock request containing the expired token as a cookie + const mockRequest = new Request("https://example.com", { + headers: { Cookie: `sAccessToken=${expiredToken}` }, + }); + + // Call the getSessionForSSR function + const result = await getSessionForSSR(mockRequest, privateKey); + + // Assertions + assert.strictEqual(result.hasToken, true, "hasToken should be true for an expired token"); + assert.strictEqual( + result.accessTokenPayload, + undefined, + "accessTokenPayload should be undefined for an expired token" + ); + assert.strictEqual(result.error, undefined, "error should be undefined for an expired token"); + }); + + it("should return an error for an invalid token", async () => { + // Assume you have an invalid token that does not match the JWKS + const invalidToken = "your-invalid-jwt-token"; + + // Create a mock request containing the invalid token as a cookie + const mockRequest = new Request("https://example.com", { + headers: { Cookie: `sAccessToken=${invalidToken}` }, + }); + + // Call the getSessionForSSR function + const result = await getSessionForSSR(mockRequest, privateKey); + + // Assertions + assert.strictEqual(result.hasToken, true, "hasToken should be true for an invalid token"); + assert.strictEqual( + result.accessTokenPayload, + undefined, + "accessTokenPayload should be undefined for an invalid token" + ); + assert.ok(result.error instanceof Error, "error should be an instance of Error for an invalid token"); + }); }); From f2e5322668bd64c476fa24cdd85fffb43809fa30 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 12:33:39 +0530 Subject: [PATCH 10/28] Fix mochar related config and command --- .mocharc.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.mocharc.yml b/.mocharc.yml index cb61a123d..509f8ef3b 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -1,4 +1,4 @@ -# spec: test/**/*.test.js +spec: test/**/*.test.js exit: true slow: 10000 timeout: 20000 diff --git a/package.json b/package.json index 4962ba606..c3ba37f46 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { - "test": "TEST_MODE=testing npx mocha --bail --node-option no-experimental-fetch -r test/fetch-polyfill.mjs --timeout 500000", + "test": "TEST_MODE=testing npx mocha --node-option no-experimental-fetch -r test/fetch-polyfill.mjs --timeout 500000", "test-exports": "./test/testExports.sh", "build-check": "cd lib && npx tsc -p tsconfig.json --noEmit && cd ../test/with-typescript && npm run build", "build": "cd lib && rm -rf build && npx tsc -p tsconfig.json && cd ../test/with-typescript && npm run build && cd ../.. && npm run post-build", From 3cdc077da719eef48d7b6589dae8b30ad19d3ecf Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 15:24:29 +0530 Subject: [PATCH 11/28] Add init changes to re-use functionality from custom framework in next --- lib/build/customFramework.d.ts | 21 +++++++++-- lib/build/customFramework.js | 42 ++++++++++++++-------- lib/build/nextjs.js | 43 +++------------------- lib/ts/customFramework.ts | 65 ++++++++++++++++++++++++---------- lib/ts/nextjs.ts | 51 ++++---------------------- 5 files changed, 104 insertions(+), 118 deletions(-) diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index eec048b93..5d67e9952 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -8,7 +8,24 @@ import { PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { JWTPayload } from "jose"; export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; -export declare function createPreParsedRequest(request: Request): PreParsedRequest; +export declare type GetCookieFn = (req: T) => Record; +export interface ParsableRequest { + url: string; + method: string; + headers: Headers; + formData: () => Promise; + json: () => Promise; +} +export declare function createPreParsedRequest( + request: RequestType, + getCookieFn?: GetCookieFn +): PreParsedRequest; +export declare function getCookieFromRequest(request: ParsableRequest): Record; +export declare function getQueryFromRequest(request: ParsableRequest): Record; +export declare function getHandleCall( + res: typeof Response, + stMiddleware: any +): (req: T) => Promise; export declare function handleAuthAPIRequest(CustomResponse: typeof Response): (req: Request) => Promise; /** * A helper function to retrieve session details on the server side. @@ -19,7 +36,7 @@ export declare function handleAuthAPIRequest(CustomResponse: typeof Response): ( */ export declare function getSessionForSSR( request: Request, - jwks: any + jwks?: any ): Promise<{ accessTokenPayload: JWTPayload | undefined; hasToken: boolean; diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index f6d54ad38..134c8c443 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -10,7 +10,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.createPreParsedRequest = void 0; +exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; const cookie_1 = require("cookie"); const custom_1 = require("./framework/custom"); const session_1 = __importDefault(require("./recipe/session")); @@ -19,14 +19,15 @@ const constants_1 = require("./recipe/session/constants"); const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); const jwt_1 = require("./recipe/session/jwt"); const jose_1 = require("jose"); -function createPreParsedRequest(request) { +const supertokens_1 = __importDefault(require("./supertokens")); +function createPreParsedRequest(request, getCookieFn = getCookieFromRequest) { /** * This helper function can take any `Request` type of object * and parse the details into an equivalent PreParsedRequest * that can be used with the custom framework helpers. */ return new custom_1.PreParsedRequest({ - cookies: getCookieFromRequest(request), + cookies: getCookieFn(request), url: request.url, method: request.method, query: getQueryFromRequest(request), @@ -56,6 +57,7 @@ function getCookieFromRequest(request) { } return cookies; } +exports.getCookieFromRequest = getCookieFromRequest; function getQueryFromRequest(request) { /** * Helper function to extract query from any `Request` type of @@ -69,6 +71,7 @@ function getQueryFromRequest(request) { }); return query; } +exports.getQueryFromRequest = getQueryFromRequest; function getAccessToken(request) { return getCookieFromRequest(request)["sAccessToken"]; } @@ -77,14 +80,7 @@ async function verifyToken(token, jwks) { const { payload } = await jose_1.jwtVerify(token, jwks); return payload; } -function handleAuthAPIRequest(CustomResponse) { - /** - * Util function to handle all calls by intercepting them, calling - * Supertokens middleware and then accordingly returning. - */ - const stMiddleware = custom_1.middleware((req) => { - return createPreParsedRequest(req); - }); +function getHandleCall(res, stMiddleware) { return async function handleCall(req) { const baseResponse = new custom_1.CollectingResponse(); const { handled, error } = await stMiddleware(req, baseResponse); @@ -92,7 +88,7 @@ function handleAuthAPIRequest(CustomResponse) { throw error; } if (!handled) { - return new CustomResponse("Not found", { status: 404 }); + return new res("Not found", { status: 404 }); } for (const respCookie of baseResponse.cookies) { baseResponse.headers.append( @@ -107,12 +103,23 @@ function handleAuthAPIRequest(CustomResponse) { }) ); } - return new CustomResponse(baseResponse.body, { + return new res(baseResponse.body, { headers: baseResponse.headers, status: baseResponse.statusCode, }); }; } +exports.getHandleCall = getHandleCall; +function handleAuthAPIRequest(CustomResponse) { + /** + * Util function to handle all calls by intercepting them, calling + * Supertokens middleware and then accordingly returning. + */ + const stMiddleware = custom_1.middleware((req) => { + return createPreParsedRequest(req); + }); + return getHandleCall(CustomResponse, stMiddleware); +} exports.handleAuthAPIRequest = handleAuthAPIRequest; async function getSessionDetails(preParsedRequest, options, userContext) { const baseResponse = new custom_1.CollectingResponse(); @@ -171,9 +178,16 @@ async function getSessionDetails(preParsedRequest, options, userContext) { async function getSessionForSSR(request, jwks) { const accessToken = getAccessToken(request); const hasToken = !!accessToken; + let jwksToUse = jwks; + if (!jwks) { + const stInstance = supertokens_1.default.getInstanceOrThrowError(); + jwksToUse = jose_1.createRemoteJWKSet( + new URL(`${stInstance.appInfo.apiDomain}${stInstance.appInfo.apiBasePath}/jwt/jwks.json`) + ); + } try { if (accessToken) { - const decoded = await verifyToken(accessToken, jwks); + const decoded = await verifyToken(accessToken, jwksToUse); return { accessTokenPayload: decoded, hasToken, error: undefined }; } return { accessTokenPayload: undefined, hasToken, error: undefined }; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 28c1a63d6..f65a3f79a 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -40,6 +40,7 @@ const recipe_1 = __importDefault(require("./recipe/session/recipe")); const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); const constants_1 = require("./recipe/session/constants"); const jwt_1 = require("./recipe/session/jwt"); +const customFramework_1 = require("./customFramework"); function next(request, response, resolve, reject) { return async function (middlewareError) { if (middlewareError === undefined) { @@ -76,46 +77,12 @@ class NextJS { }); } static getAppDirRequestHandler(NextResponse) { + const getCookieFromNextReq = (req) => + Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); const stMiddleware = custom_1.middleware((req) => { - const query = Object.fromEntries(new URL(req.url).searchParams.entries()); - const cookies = Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); - return new custom_1.PreParsedRequest({ - method: req.method, - url: req.url, - query: query, - headers: req.headers, - cookies, - getFormBody: () => req.formData(), - getJSONBody: () => req.json(), - }); + return customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); }); - return async function handleCall(req) { - const baseResponse = new custom_1.CollectingResponse(); - const { handled, error } = await stMiddleware(req, baseResponse); - if (error) { - throw error; - } - if (!handled) { - return new NextResponse("Not found", { status: 404 }); - } - for (const respCookie of baseResponse.cookies) { - baseResponse.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, - }) - ); - } - return new NextResponse(baseResponse.body, { - headers: baseResponse.headers, - status: baseResponse.statusCode, - }); - }; + return customFramework_1.getHandleCall(NextResponse, stMiddleware); } static async commonSSRSession(baseRequest, options, userContext) { let baseResponse = new custom_1.CollectingResponse(); diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 9d0ff28bb..de95c9762 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -11,19 +11,33 @@ import SessionRecipe from "./recipe/session/recipe"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; -import { jwtVerify, JWTPayload } from "jose"; +import { jwtVerify, JWTPayload, createRemoteJWKSet } from "jose"; +import SuperTokens from "./supertokens"; // Define supported types for HTTPMethod export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; -export function createPreParsedRequest(request: Request): PreParsedRequest { +export type GetCookieFn = (req: T) => Record; + +export interface ParsableRequest { + url: string; + method: string; + headers: Headers; + formData: () => Promise; + json: () => Promise; +} + +export function createPreParsedRequest( + request: RequestType, + getCookieFn: GetCookieFn = getCookieFromRequest +): PreParsedRequest { /** * This helper function can take any `Request` type of object * and parse the details into an equivalent PreParsedRequest * that can be used with the custom framework helpers. */ return new PreParsedRequest({ - cookies: getCookieFromRequest(request), + cookies: getCookieFn(request), url: request.url as string, method: request.method as HTTPMethod, query: getQueryFromRequest(request), @@ -37,7 +51,7 @@ export function createPreParsedRequest(request: Request): PreParsedRequest { }); } -function getCookieFromRequest(request: Request): Record { +export function getCookieFromRequest(request: ParsableRequest): Record { /** * This function will extract the cookies from any `Request` * type of object and return them to be usable with PreParsedRequest. @@ -54,7 +68,7 @@ function getCookieFromRequest(request: Request): Record { return cookies; } -function getQueryFromRequest(request: Request): Record { +export function getQueryFromRequest(request: ParsableRequest): Record { /** * Helper function to extract query from any `Request` type of * object and return them to be usable with PreParsedRequest. @@ -78,16 +92,8 @@ async function verifyToken(token: string, jwks: any): Promise { return payload; } -export function handleAuthAPIRequest(CustomResponse: typeof Response) { - /** - * Util function to handle all calls by intercepting them, calling - * Supertokens middleware and then accordingly returning. - */ - const stMiddleware = middleware((req) => { - return createPreParsedRequest(req); - }); - - return async function handleCall(req: Request) { +export function getHandleCall(res: typeof Response, stMiddleware: any) { + return async function handleCall(req: T) { const baseResponse = new CollectingResponse(); const { handled, error } = await stMiddleware(req, baseResponse); @@ -96,7 +102,7 @@ export function handleAuthAPIRequest(CustomResponse: typeof Response) { throw error; } if (!handled) { - return new CustomResponse("Not found", { status: 404 }); + return new res("Not found", { status: 404 }); } for (const respCookie of baseResponse.cookies) { @@ -113,13 +119,25 @@ export function handleAuthAPIRequest(CustomResponse: typeof Response) { ); } - return new CustomResponse(baseResponse.body, { + return new res(baseResponse.body, { headers: baseResponse.headers, status: baseResponse.statusCode, }); }; } +export function handleAuthAPIRequest(CustomResponse: typeof Response) { + /** + * Util function to handle all calls by intercepting them, calling + * Supertokens middleware and then accordingly returning. + */ + const stMiddleware = middleware((req) => { + return createPreParsedRequest(req); + }); + + return getHandleCall(CustomResponse, stMiddleware); +} + async function getSessionDetails( preParsedRequest: PreParsedRequest, options?: VerifySessionOptions, @@ -187,7 +205,7 @@ async function getSessionDetails( */ export async function getSessionForSSR( request: Request, - jwks: any + jwks?: any ): Promise<{ accessTokenPayload: JWTPayload | undefined; hasToken: boolean; @@ -195,9 +213,18 @@ export async function getSessionForSSR( }> { const accessToken = getAccessToken(request); const hasToken = !!accessToken; + + let jwksToUse = jwks; + if (!jwks) { + const stInstance = SuperTokens.getInstanceOrThrowError(); + jwksToUse = createRemoteJWKSet( + new URL(`${stInstance.appInfo.apiDomain}${stInstance.appInfo.apiBasePath}/jwt/jwks.json`) + ); + } + try { if (accessToken) { - const decoded = await verifyToken(accessToken, jwks); + const decoded = await verifyToken(accessToken, jwksToUse); return { accessTokenPayload: decoded, hasToken, error: undefined }; } return { accessTokenPayload: undefined, hasToken, error: undefined }; diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index 266213b22..aff367f49 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -28,6 +28,7 @@ import SessionRecipe from "./recipe/session/recipe"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; +import { createPreParsedRequest, GetCookieFn, getHandleCall } from "./customFramework"; function next( request: any, @@ -91,54 +92,14 @@ export default class NextJS { } static getAppDirRequestHandler(NextResponse: typeof Response) { - const stMiddleware = middleware((req) => { - const query = Object.fromEntries(new URL(req.url).searchParams.entries()); - const cookies: Record = Object.fromEntries( - req.cookies.getAll().map((cookie) => [cookie.name, cookie.value]) - ); + const getCookieFromNextReq: GetCookieFn = (req: T): Record => + Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); - return new PreParsedRequest({ - method: req.method as HTTPMethod, - url: req.url, - query: query, - headers: req.headers, - cookies, - getFormBody: () => req.formData(), - getJSONBody: () => req.json(), - }); + const stMiddleware = middleware((req) => { + return createPreParsedRequest(req, getCookieFromNextReq); }); - return async function handleCall(req: T) { - const baseResponse = new CollectingResponse(); - - const { handled, error } = await stMiddleware(req, baseResponse); - - if (error) { - throw error; - } - if (!handled) { - return new NextResponse("Not found", { status: 404 }); - } - - for (const respCookie of baseResponse.cookies) { - baseResponse.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, - }) - ); - } - - return new NextResponse(baseResponse.body, { - headers: baseResponse.headers, - status: baseResponse.statusCode, - }); - }; + return getHandleCall(NextResponse, stMiddleware); } private static async commonSSRSession( From e7d812bf44ab602bd3fd8242998fb3f116ef6aad Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 15:27:30 +0530 Subject: [PATCH 12/28] Fix export name for customframework and version --- CHANGELOG.md | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aab4cd538..1dd1e4581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a way to run CI on unmerged PRs - Added support for FDIs: 3.1 and 4.0. Required by: auth-react >=0.49.0 and web-js>=0.15.0 - The `networkInterceptor` now also gets a new `params` prop in the request config. -- Adds `customFramework` util functions to minimize code required in custom frameworks like remix, astro etc. +- Adds `customFramework` util functions to minimize code required in custom frameworks like remix, astro etc. ### Breaking change diff --git a/package.json b/package.json index c3ba37f46..44d7e8773 100644 --- a/package.json +++ b/package.json @@ -65,15 +65,15 @@ "types": "./nextjs/index.d.ts", "default": "./nextjs/index.js" }, - "./customFramework": { + "./customframework": { "types": "./custom/index.d.ts", "default": "./custom/index.js" }, - "./customFramework/index": { + "./customframework/index": { "types": "./custom/index.d.ts", "default": "./custom/index.js" }, - "./customFramework/index.js": { + "./customframework/index.js": { "types": "./custom/index.d.ts", "default": "./custom/index.js" }, From 6e241d95d20e6fd4493f704c7298608b3ca07b04 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 15:59:47 +0530 Subject: [PATCH 13/28] Add support for re-using duplicated code in nextjs from customframework --- lib/build/customFramework.d.ts | 11 +++- lib/build/customFramework.js | 95 ++++++++++++++++++------------ lib/build/nextjs.d.ts | 2 + lib/build/nextjs.js | 66 +++------------------ lib/ts/customFramework.ts | 104 ++++++++++++++++++++------------- lib/ts/nextjs.ts | 77 ++++-------------------- 6 files changed, 153 insertions(+), 202 deletions(-) diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 5d67e9952..169b3cc4d 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -4,7 +4,7 @@ * that can be used to easily integrate the SDK with most * frameworks if they are not directly supported. */ -import { PreParsedRequest } from "./framework/custom"; +import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { JWTPayload } from "jose"; export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; @@ -48,3 +48,12 @@ export declare function withSession( options?: VerifySessionOptions, userContext?: Record ): Promise; +export declare function addCookies( + baseResponse: CollectingResponse, + userResponse: UserResponseType +): UserResponseType; +export declare function handleError( + err: any, + baseRequest: PreParsedRequest, + baseResponse: CollectingResponse +): Promise; diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 134c8c443..369c092ab 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -10,7 +10,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; +exports.handleError = exports.addCookies = exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; const cookie_1 = require("cookie"); const custom_1 = require("./framework/custom"); const session_1 = __importDefault(require("./recipe/session")); @@ -210,46 +210,65 @@ async function withSession(request, handler, options, userContext) { try { userResponse = await handler(undefined, session); } catch (err) { - await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); + 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", - 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); - }); - 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; + return addCookies(baseResponse, userResponse); } catch (error) { return await handler(error, undefined); } } exports.withSession = withSession; +function addCookies(baseResponse, userResponse) { + /** + * Add cookies to the userResponse passed by copying it from the 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; +} +exports.addCookies = addCookies; +async function handleError(err, baseRequest, baseResponse) { + await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { + if (errorHandlerError) { + throw errorHandlerError; + } + }); + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + return new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }); +} +exports.handleError = handleError; diff --git a/lib/build/nextjs.d.ts b/lib/build/nextjs.d.ts index 7b06b2936..038c70f52 100644 --- a/lib/build/nextjs.d.ts +++ b/lib/build/nextjs.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; +import { GetCookieFn } from "./customFramework"; declare type PartialNextRequest = { method: string; url: string; @@ -20,6 +21,7 @@ export default class NextJS { request: any, response: any ): Promise; + static getCookieExtractor(): GetCookieFn; static getAppDirRequestHandler( NextResponse: typeof Response ): (req: T) => Promise; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index f65a3f79a..80f07d72b 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -76,9 +76,11 @@ class NextJS { } }); } + static getCookieExtractor() { + return (req) => Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + } static getAppDirRequestHandler(NextResponse) { - const getCookieFromNextReq = (req) => - Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + const getCookieFromNextReq = this.getCookieExtractor(); const stMiddleware = custom_1.middleware((req) => { return customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); }); @@ -222,68 +224,16 @@ class NextJS { } } static async withPreParsedRequestResponse(req, handler) { - 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 getCookieFromNextReq = this.getCookieExtractor(); + let baseRequest = customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); let baseResponse = new custom_1.CollectingResponse(); let userResponse; try { userResponse = await handler(baseRequest, baseResponse); } catch (err) { - await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); - } - 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"); - } + userResponse = await customFramework_1.handleError(err, baseRequest, baseResponse); } - return userResponse; + return customFramework_1.addCookies(baseResponse, userResponse); } } exports.default = NextJS; diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index de95c9762..661d16627 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -255,50 +255,76 @@ export async function withSession( try { userResponse = await handler(undefined, session); } catch (err) { - await errorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); + userResponse = await handleError(err, baseRequest, baseResponse); } - let didAddCookies = false; - let didAddHeaders = false; + return addCookies(baseResponse, userResponse); + } catch (error) { + return await handler(error as Error, undefined); + } +} - 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, - }) - ); +export function addCookies( + baseResponse: CollectingResponse, + userResponse: UserResponseType +): UserResponseType { + /** + * Add cookies to the userResponse passed by copying it from the 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; +} - baseResponse.headers.forEach((value: string, key: string) => { - didAddHeaders = true; - userResponse.headers.set(key, value); - }); - 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"); - } +export async function handleError( + err: any, + baseRequest: PreParsedRequest, + baseResponse: CollectingResponse +): Promise { + await errorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { + if (errorHandlerError) { + throw errorHandlerError; } + }); - return userResponse; - } catch (error) { - return await handler(error as Error, undefined); - } + // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. + return new Response(baseResponse.body, { + status: baseResponse.statusCode, + headers: baseResponse.headers, + }) as UserResponseType; } diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index aff367f49..1c19383cf 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -28,7 +28,7 @@ import SessionRecipe from "./recipe/session/recipe"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; -import { createPreParsedRequest, GetCookieFn, getHandleCall } from "./customFramework"; +import { addCookies, createPreParsedRequest, GetCookieFn, getHandleCall, handleError } from "./customFramework"; function next( request: any, @@ -91,9 +91,13 @@ export default class NextJS { }); } - static getAppDirRequestHandler(NextResponse: typeof Response) { - const getCookieFromNextReq: GetCookieFn = (req: T): Record => + static getCookieExtractor(): GetCookieFn { + return (req: T): Record => Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + } + + static getAppDirRequestHandler(NextResponse: typeof Response) { + const getCookieFromNextReq = this.getCookieExtractor(); const stMiddleware = middleware((req) => { return createPreParsedRequest(req, getCookieFromNextReq); @@ -291,20 +295,8 @@ export default class NextJS { req: NextRequest, handler: (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => Promise ): Promise { - 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 getCookieFromNextReq = this.getCookieExtractor(); + let baseRequest = createPreParsedRequest(req, getCookieFromNextReq); let baseResponse = new CollectingResponse(); let userResponse: NextResponse; @@ -312,57 +304,10 @@ export default class NextJS { try { userResponse = await handler(baseRequest, baseResponse); } catch (err) { - await customErrorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }) as NextResponse; - } - - 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, - }) - ); + userResponse = await handleError(err, baseRequest, baseResponse); } - 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; + return addCookies(baseResponse, userResponse); } } export let superTokensNextWrapper = NextJS.superTokensNextWrapper; From a363f75d4e4b3313c961c3638ca6e1b3a026cd8f Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 16:08:59 +0530 Subject: [PATCH 14/28] Reuse even more code from customframework into next.js --- lib/build/customFramework.d.ts | 12 ++++- lib/build/customFramework.js | 3 +- lib/build/nextjs.d.ts | 1 - lib/build/nextjs.js | 68 +++------------------------ lib/ts/customFramework.ts | 6 +-- lib/ts/nextjs.ts | 84 ++++++---------------------------- 6 files changed, 35 insertions(+), 139 deletions(-) diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 169b3cc4d..7f672cef8 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -7,7 +7,6 @@ import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { JWTPayload } from "jose"; -export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; export declare type GetCookieFn = (req: T) => Record; export interface ParsableRequest { url: string; @@ -27,6 +26,17 @@ export declare function getHandleCall( stMiddleware: any ): (req: T) => Promise; export declare function handleAuthAPIRequest(CustomResponse: typeof Response): (req: Request) => Promise; +export declare function getSessionDetails( + preParsedRequest: PreParsedRequest, + options?: VerifySessionOptions, + userContext?: Record +): Promise<{ + session: SessionContainer | undefined; + hasToken: boolean; + hasInvalidClaims: boolean; + baseResponse: CollectingResponse; + response?: Response; +}>; /** * A helper function to retrieve session details on the server side. * diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 369c092ab..2f38523a7 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -10,7 +10,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.handleError = exports.addCookies = exports.withSession = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; +exports.handleError = exports.addCookies = exports.withSession = exports.getSessionForSSR = exports.getSessionDetails = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; const cookie_1 = require("cookie"); const custom_1 = require("./framework/custom"); const session_1 = __importDefault(require("./recipe/session")); @@ -168,6 +168,7 @@ async function getSessionDetails(preParsedRequest, options, userContext) { } } } +exports.getSessionDetails = getSessionDetails; /** * A helper function to retrieve session details on the server side. * diff --git a/lib/build/nextjs.d.ts b/lib/build/nextjs.d.ts index 038c70f52..79ba7a644 100644 --- a/lib/build/nextjs.d.ts +++ b/lib/build/nextjs.d.ts @@ -25,7 +25,6 @@ export default class NextJS { static getAppDirRequestHandler( NextResponse: typeof Response ): (req: T) => Promise; - private static commonSSRSession; static getSSRSession( cookies: Array<{ name: string; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 80f07d72b..9599065a7 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -24,22 +24,12 @@ var __rest = } return t; }; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; 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"); -const session_1 = __importDefault(require("./recipe/session")); -const recipe_1 = __importDefault(require("./recipe/session/recipe")); -const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); -const constants_1 = require("./recipe/session/constants"); -const jwt_1 = require("./recipe/session/jwt"); const customFramework_1 = require("./customFramework"); function next(request, response, resolve, reject) { return async function (middlewareError) { @@ -86,52 +76,6 @@ class NextJS { }); return customFramework_1.getHandleCall(NextResponse, stMiddleware); } - static async commonSSRSession(baseRequest, options, userContext) { - let baseResponse = new custom_1.CollectingResponse(); - const recipe = recipe_1.default.getInstanceOrThrowError(); - const tokenTransferMethod = recipe.config.getTokenTransferMethod({ - req: baseRequest, - forCreateNewSession: false, - userContext, - }); - const transferMethods = - tokenTransferMethod === "any" ? constants_1.availableTokenTransferMethods : [tokenTransferMethod]; - const hasToken = transferMethods.some((transferMethod) => { - const token = cookieAndHeaders_1.getToken(baseRequest, "access", transferMethod); - if (!token) { - return false; - } - try { - jwt_1.parseJWTWithoutSignatureVerification(token); - return true; - } catch (_a) { - return false; - } - }); - try { - let session = await session_1.default.getSession(baseRequest, baseResponse, options, userContext); - return { - session, - hasInvalidClaims: false, - hasToken, - baseResponse, - }; - } catch (err) { - if (session_1.default.Error.isErrorFromSuperTokens(err)) { - return { - hasToken, - hasInvalidClaims: err.type === session_1.default.Error.INVALID_CLAIMS, - session: undefined, - baseResponse, - nextResponse: new Response("Authentication required", { - status: err.type === session_1.default.Error.INVALID_CLAIMS ? 403 : 401, - }), - }; - } else { - throw err; - } - } - } static async getSSRSession(cookies, headers, options, userContext) { let cookiesObj = Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value])); let baseRequest = new custom_1.PreParsedRequest({ @@ -143,9 +87,9 @@ class NextJS { getFormBody: async () => [], getJSONBody: async () => [], }); - const _a = await NextJS.commonSSRSession(baseRequest, options, utils_1.getUserContext(userContext)), - { baseResponse, nextResponse } = _a, - result = __rest(_a, ["baseResponse", "nextResponse"]); + const _a = await customFramework_1.getSessionDetails(baseRequest, options, utils_1.getUserContext(userContext)), + { baseResponse, response } = _a, + result = __rest(_a, ["baseResponse", "response"]); return result; } static async withSession(req, handler, options, userContext) { @@ -161,13 +105,13 @@ class NextJS { getFormBody: () => req.formData(), getJSONBody: () => req.json(), }); - const { session, nextResponse, baseResponse } = await NextJS.commonSSRSession( + const { session, response, baseResponse } = await customFramework_1.getSessionDetails( baseRequest, options, utils_1.getUserContext(userContext) ); - if (nextResponse) { - return nextResponse; + if (response) { + return response; } let userResponse; try { diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 661d16627..2b3ec777d 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -13,9 +13,7 @@ import { getToken } from "./recipe/session/cookieAndHeaders"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; import { jwtVerify, JWTPayload, createRemoteJWKSet } from "jose"; import SuperTokens from "./supertokens"; - -// Define supported types for HTTPMethod -export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; +import { HTTPMethod } from "./types"; export type GetCookieFn = (req: T) => Record; @@ -138,7 +136,7 @@ export function handleAuthAPIRequest(CustomResponse: typeof Response) { return getHandleCall(CustomResponse, stMiddleware); } -async function getSessionDetails( +export async function getSessionDetails( preParsedRequest: PreParsedRequest, options?: VerifySessionOptions, userContext?: Record diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index 1c19383cf..4df0abebe 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -22,13 +22,16 @@ import { middleware, errorHandler as customErrorHandler, } from "./framework/custom"; -import { HTTPMethod, UserContext } from "./types"; -import Session, { SessionContainer, VerifySessionOptions } from "./recipe/session"; -import SessionRecipe from "./recipe/session/recipe"; -import { getToken } from "./recipe/session/cookieAndHeaders"; -import { availableTokenTransferMethods } from "./recipe/session/constants"; -import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; -import { addCookies, createPreParsedRequest, GetCookieFn, getHandleCall, handleError } from "./customFramework"; +import { HTTPMethod } from "./types"; +import { SessionContainer, VerifySessionOptions } from "./recipe/session"; +import { + addCookies, + createPreParsedRequest, + GetCookieFn, + getHandleCall, + getSessionDetails, + handleError, +} from "./customFramework"; function next( request: any, @@ -106,65 +109,6 @@ export default class NextJS { return getHandleCall(NextResponse, stMiddleware); } - private static async commonSSRSession( - baseRequest: PreParsedRequest, - options: VerifySessionOptions | undefined, - userContext: UserContext - ): Promise<{ - session: SessionContainer | undefined; - hasToken: boolean; - hasInvalidClaims: boolean; - baseResponse: CollectingResponse; - nextResponse?: Response; - }> { - let baseResponse = new CollectingResponse(); - - const recipe = SessionRecipe.getInstanceOrThrowError(); - const tokenTransferMethod = recipe.config.getTokenTransferMethod({ - req: baseRequest, - forCreateNewSession: false, - userContext, - }); - const transferMethods = tokenTransferMethod === "any" ? availableTokenTransferMethods : [tokenTransferMethod]; - const hasToken = transferMethods.some((transferMethod) => { - const token = getToken(baseRequest, "access", transferMethod); - if (!token) { - return false; - } - - try { - parseJWTWithoutSignatureVerification(token); - return true; - } catch { - return false; - } - }); - - try { - let session = await Session.getSession(baseRequest, baseResponse, options, userContext); - return { - session, - hasInvalidClaims: false, - hasToken, - baseResponse, - }; - } catch (err) { - if (Session.Error.isErrorFromSuperTokens(err)) { - return { - hasToken, - hasInvalidClaims: err.type === Session.Error.INVALID_CLAIMS, - session: undefined, - baseResponse, - nextResponse: new Response("Authentication required", { - status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401, - }), - }; - } else { - throw err; - } - } - } - static async getSSRSession( cookies: Array<{ name: string; value: string }>, headers: Headers, @@ -189,7 +133,7 @@ export default class NextJS { getJSONBody: async () => [], }); - const { baseResponse, nextResponse, ...result } = await NextJS.commonSSRSession( + const { baseResponse, response, ...result } = await getSessionDetails( baseRequest, options, getUserContext(userContext) @@ -219,14 +163,14 @@ export default class NextJS { getJSONBody: () => req!.json(), }); - const { session, nextResponse, baseResponse } = await NextJS.commonSSRSession( + const { session, response, baseResponse } = await getSessionDetails( baseRequest, options, getUserContext(userContext) ); - if (nextResponse) { - return nextResponse as NextResponse; + if (response) { + return response as NextResponse; } let userResponse: NextResponse; From db4a6576dc8f77a3247a7f7068e65b70d279dc89 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 16:23:45 +0530 Subject: [PATCH 15/28] Reuse a customError handler in next --- lib/build/nextjs.js | 11 +---------- lib/ts/nextjs.ts | 19 ++----------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 9599065a7..517cdad9e 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -117,16 +117,7 @@ class NextJS { try { userResponse = await handler(undefined, session); } catch (err) { - await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }); + userResponse = await customFramework_1.handleError(err, baseRequest, baseResponse); } let didAddCookies = false; let didAddHeaders = false; diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index 4df0abebe..325a1e733 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -16,12 +16,7 @@ import { serialize } from "cookie"; import { errorHandler } from "./framework/express"; import { getUserContext } from "./utils"; -import { - CollectingResponse, - PreParsedRequest, - middleware, - errorHandler as customErrorHandler, -} from "./framework/custom"; +import { CollectingResponse, PreParsedRequest, middleware } from "./framework/custom"; import { HTTPMethod } from "./types"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { @@ -178,17 +173,7 @@ export default class NextJS { try { userResponse = await handler(undefined, session); } catch (err) { - await customErrorHandler()(err, baseRequest, baseResponse, (errorHandlerError: Error) => { - if (errorHandlerError) { - throw errorHandlerError; - } - }); - - // The headers in the userResponse are set twice from baseResponse, but the resulting response contains unique headers. - userResponse = new Response(baseResponse.body, { - status: baseResponse.statusCode, - headers: baseResponse.headers, - }) as NextResponse; + userResponse = await handleError(err, baseRequest, baseResponse); } let didAddCookies = false; From 40fb2d751e2e18ccdc0209864390eb54265cf48a Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Thu, 3 Oct 2024 18:09:43 +0530 Subject: [PATCH 16/28] Update customFramework to handle JWT verification manually --- lib/build/customFramework.js | 4 +++- lib/ts/customFramework.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 2f38523a7..f4794a458 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -183,7 +183,9 @@ async function getSessionForSSR(request, jwks) { if (!jwks) { const stInstance = supertokens_1.default.getInstanceOrThrowError(); jwksToUse = jose_1.createRemoteJWKSet( - new URL(`${stInstance.appInfo.apiDomain}${stInstance.appInfo.apiBasePath}/jwt/jwks.json`) + new URL( + `${stInstance.appInfo.apiDomain.getAsStringDangerous()}${stInstance.appInfo.apiBasePath.getAsStringDangerous()}/jwt/jwks.json` + ) ); } try { diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 2b3ec777d..6049d5f49 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -216,7 +216,9 @@ export async function getSessionForSSR( if (!jwks) { const stInstance = SuperTokens.getInstanceOrThrowError(); jwksToUse = createRemoteJWKSet( - new URL(`${stInstance.appInfo.apiDomain}${stInstance.appInfo.apiBasePath}/jwt/jwks.json`) + new URL( + `${stInstance.appInfo.apiDomain.getAsStringDangerous()}${stInstance.appInfo.apiBasePath.getAsStringDangerous()}/jwt/jwks.json` + ) ); } From 0bf1533e75e6bb52c63d8548f6f7aa446739af0b Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Fri, 4 Oct 2024 09:33:57 +0530 Subject: [PATCH 17/28] Use internal functions for jwt parsing --- lib/build/customFramework.js | 11 ++--- lib/ts/customFramework.ts | 14 +++--- test/customFramework.test.js | 84 +++++++++++++++++++++++++++--------- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index f4794a458..9326c42ef 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -20,6 +20,7 @@ const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); const jwt_1 = require("./recipe/session/jwt"); const jose_1 = require("jose"); const supertokens_1 = __importDefault(require("./supertokens")); +const accessToken_1 = require("./recipe/session/accessToken"); function createPreParsedRequest(request, getCookieFn = getCookieFromRequest) { /** * This helper function can take any `Request` type of object @@ -75,11 +76,6 @@ exports.getQueryFromRequest = getQueryFromRequest; function getAccessToken(request) { return getCookieFromRequest(request)["sAccessToken"]; } -async function verifyToken(token, jwks) { - // Verify the JWT using the remote JWK set and return the payload - const { payload } = await jose_1.jwtVerify(token, jwks); - return payload; -} function getHandleCall(res, stMiddleware) { return async function handleCall(req) { const baseResponse = new custom_1.CollectingResponse(); @@ -190,8 +186,9 @@ async function getSessionForSSR(request, jwks) { } try { if (accessToken) { - const decoded = await verifyToken(accessToken, jwksToUse); - return { accessTokenPayload: decoded, hasToken, error: undefined }; + const tokenInfo = jwt_1.parseJWTWithoutSignatureVerification(accessToken); + const decoded = await accessToken_1.getInfoFromAccessToken(tokenInfo, jwksToUse, false); + return { accessTokenPayload: decoded.userData, hasToken, error: undefined }; } return { accessTokenPayload: undefined, hasToken, error: undefined }; } catch (error) { diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 6049d5f49..544685fb1 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -11,9 +11,10 @@ import SessionRecipe from "./recipe/session/recipe"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; -import { jwtVerify, JWTPayload, createRemoteJWKSet } from "jose"; +import { JWTPayload, createRemoteJWKSet } from "jose"; import SuperTokens from "./supertokens"; import { HTTPMethod } from "./types"; +import { getInfoFromAccessToken } from "./recipe/session/accessToken"; export type GetCookieFn = (req: T) => Record; @@ -84,12 +85,6 @@ function getAccessToken(request: Request): string | undefined { return getCookieFromRequest(request)["sAccessToken"]; } -async function verifyToken(token: string, jwks: any): Promise { - // Verify the JWT using the remote JWK set and return the payload - const { payload } = await jwtVerify(token, jwks); - return payload; -} - export function getHandleCall(res: typeof Response, stMiddleware: any) { return async function handleCall(req: T) { const baseResponse = new CollectingResponse(); @@ -224,8 +219,9 @@ export async function getSessionForSSR( try { if (accessToken) { - const decoded = await verifyToken(accessToken, jwksToUse); - return { accessTokenPayload: decoded, hasToken, error: undefined }; + const tokenInfo = parseJWTWithoutSignatureVerification(accessToken); + const decoded = await getInfoFromAccessToken(tokenInfo, jwksToUse, false); + return { accessTokenPayload: decoded.userData, hasToken, error: undefined }; } return { accessTokenPayload: undefined, hasToken, error: undefined }; } catch (error) { diff --git a/test/customFramework.test.js b/test/customFramework.test.js index c010b0da4..aee2a5ab2 100644 --- a/test/customFramework.test.js +++ b/test/customFramework.test.js @@ -10,19 +10,56 @@ let SuperTokens = require("../lib/build/").default; const Session = require("../lib/build/recipe/session"); const EmailPassword = require("../lib/build/recipe/emailpassword"); const { PreParsedRequest } = require("../lib/build/framework/custom"); -const { printPath, setupST, startST, killAllST, cleanST, delay } = require("./utils"); -const { generateKeyPair, SignJWT } = require("jose"); +const { printPath, setupST, startST, killAllST, cleanST } = require("./utils"); +const { generateKeyPair, SignJWT, exportJWK, importJWK, decodeJwt } = require("jose"); // Helper function to create a JWKS async function createJWKS() { - const { privateKey } = await generateKeyPair("RS256"); - return privateKey; + // Generate an RSA key pair + const { privateKey, publicKey } = await generateKeyPair("RS256"); + + // Export the public key to JWK format + const jwk = await exportJWK(publicKey); + + // Construct the JWKS + const jwks = { + keys: [ + { + ...jwk, + alg: "RS256", + use: "sig", + kid: "test-key-id", + }, + ], + }; + + return { privateKey, jwks }; +} + +async function createJWTVerifyGetKey(jwks) { + // Find the JWK in the set based on `kid` + const jwk = jwks.keys.find((k) => k.kid === "test-key-id"); + + if (!jwk) { + throw new Error("Key with the specified kid not found in JWKS"); + } + + // Import the JWK as a CryptoKey suitable for RS256 verification + return await importJWK(jwk, "RS256"); } // Function to sign a JWT -async function signJWT(privateKey, payload, expiresIn = "2h") { +async function signJWT(privateKey, jwks, payload, expiresIn = "2h") { + // Find the corresponding public key in the JWKS to get the `kid` and `alg` + const publicJWK = jwks.keys.find((k) => k.kid === "test-key-id"); + + if (!publicJWK) { + throw new Error("Key with the specified kid not found in JWKS"); + } + + // Sign the JWT using the private key return new SignJWT(payload) - .setProtectedHeader({ alg: "RS256", kid: "test-key-id" }) + .setProtectedHeader({ alg: publicJWK.alg, kid: publicJWK.kid, version: "5", typ: "JWT" }) .setIssuedAt() .setExpirationTime(expiresIn) .sign(privateKey); @@ -84,8 +121,8 @@ describe(`createPreParsedRequest ${printPath("[test/customFramework.test.js]")}` describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, () => { let connectionURI; - let accessToken; - let privateKey; + let accessToken, accessTokenPayload; + let privateKey, jwks; before(async function () { process.env.user = undefined; @@ -122,7 +159,9 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, ], }); - privateKey = await createJWKS(); + const { privateKey: privateKeyGenerated, jwks: jwksGenerated } = await createJWKS(); + privateKey = privateKeyGenerated; + jwks = jwksGenerated; }); after(async function () { @@ -202,6 +241,7 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, ); accessToken = response.headers.get("st-access-token"); + accessTokenPayload = decodeJwt(accessToken); assert.ok(accessToken, "st-access-token header should be set"); assert.ok(response.headers.get("st-refresh-token"), "st-refresh-token header should be set"); @@ -309,12 +349,14 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, assert.strictEqual(await response.text(), "Not found", "Should return Not found"); }); - it("getSessionForSSR should return session for valid token", async () => { - // Create a valid JWT payload - const payload = { userId: "123", email: "john.doe@example.com" }; + // NOTE: For all the JWT related testing, we are using a different key because + // the default way of getting the key is by hitting the `/jwt/jwks` endpoint + // but that endpoint doesn't return anything for testing and thus we are testing + // with a custom key. + it("getSessionForSSR should return session for valid token", async () => { // Sign the JWT - const validToken = await signJWT(privateKey, payload); + const validToken = await signJWT(privateKey, jwks, accessTokenPayload); // Create a mock request containing the valid token as a cookie const mockRequest = new Request("https://example.com", { @@ -322,14 +364,13 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, }); // Call the getSessionForSSR function - const result = await getSessionForSSR(mockRequest, privateKey); + const result = await getSessionForSSR(mockRequest, await createJWTVerifyGetKey(jwks)); // Assertions assert.strictEqual(result.hasToken, true, "hasToken should be true for a valid token"); assert.ok(result.accessTokenPayload, "accessTokenPayload should be present for a valid token"); assert.strictEqual(result.error, undefined, "error should be undefined for a valid token"); - assert.strictEqual(result.accessTokenPayload.userId, "123", "User ID in payload should match"); - assert.strictEqual(result.accessTokenPayload.email, "john.doe@example.com", "Email in payload should match"); + assert.strictEqual(result.accessTokenPayload.sub, accessTokenPayload.sub, "User ID in payload should match"); }); it("should return undefined accessTokenPayload and hasToken as false when no token is present", async () => { @@ -350,11 +391,8 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, }); it("should handle an expired token gracefully", async () => { - // Create a payload for the token - const payload = { userId: "123", email: "john.doe@example.com" }; - // Sign the JWT with an expiration time in the past (e.g., 1 second ago) - const expiredToken = await signJWT(privateKey, payload, Math.floor(Date.now() / 1000) - 1); + const expiredToken = await signJWT(privateKey, jwks, accessTokenPayload, Math.floor(Date.now() / 1000) - 1); // Create a mock request containing the expired token as a cookie const mockRequest = new Request("https://example.com", { @@ -371,7 +409,11 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, undefined, "accessTokenPayload should be undefined for an expired token" ); - assert.strictEqual(result.error, undefined, "error should be undefined for an expired token"); + assert.strictEqual( + result.error.type, + "TRY_REFRESH_TOKEN", + "error should be TRY_REFRESH_TOKEN for an expired token" + ); }); it("should return an error for an invalid token", async () => { From 9f6e7f3605fdff929f17ca7f811242b6b06a3254 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Fri, 4 Oct 2024 09:54:54 +0530 Subject: [PATCH 18/28] Add a fix for accessing nextjs cookie extractor --- lib/build/nextjs.js | 4 ++-- lib/ts/nextjs.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 517cdad9e..777538c84 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -70,7 +70,7 @@ class NextJS { return (req) => Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); } static getAppDirRequestHandler(NextResponse) { - const getCookieFromNextReq = this.getCookieExtractor(); + const getCookieFromNextReq = NextJS.getCookieExtractor(); const stMiddleware = custom_1.middleware((req) => { return customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); }); @@ -159,7 +159,7 @@ class NextJS { } } static async withPreParsedRequestResponse(req, handler) { - const getCookieFromNextReq = this.getCookieExtractor(); + const getCookieFromNextReq = NextJS.getCookieExtractor(); let baseRequest = customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); let baseResponse = new custom_1.CollectingResponse(); let userResponse; diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index 325a1e733..2e71148c3 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -95,7 +95,7 @@ export default class NextJS { } static getAppDirRequestHandler(NextResponse: typeof Response) { - const getCookieFromNextReq = this.getCookieExtractor(); + const getCookieFromNextReq = NextJS.getCookieExtractor(); const stMiddleware = middleware((req) => { return createPreParsedRequest(req, getCookieFromNextReq); @@ -224,7 +224,7 @@ export default class NextJS { req: NextRequest, handler: (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => Promise ): Promise { - const getCookieFromNextReq = this.getCookieExtractor(); + const getCookieFromNextReq = NextJS.getCookieExtractor(); let baseRequest = createPreParsedRequest(req, getCookieFromNextReq); let baseResponse = new CollectingResponse(); From c9fee10393fa515e770d26ea8759fa4bde31db19 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Fri, 4 Oct 2024 12:45:38 +0530 Subject: [PATCH 19/28] Reuse an util function for getting JWK's --- lib/build/customFramework.js | 11 +++-------- lib/ts/customFramework.ts | 12 ++++-------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 9326c42ef..e5e63fa76 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -18,9 +18,8 @@ const recipe_1 = __importDefault(require("./recipe/session/recipe")); const constants_1 = require("./recipe/session/constants"); const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); const jwt_1 = require("./recipe/session/jwt"); -const jose_1 = require("jose"); -const supertokens_1 = __importDefault(require("./supertokens")); const accessToken_1 = require("./recipe/session/accessToken"); +const combinedRemoteJWKSet_1 = require("./combinedRemoteJWKSet"); function createPreParsedRequest(request, getCookieFn = getCookieFromRequest) { /** * This helper function can take any `Request` type of object @@ -177,12 +176,8 @@ async function getSessionForSSR(request, jwks) { const hasToken = !!accessToken; let jwksToUse = jwks; if (!jwks) { - const stInstance = supertokens_1.default.getInstanceOrThrowError(); - jwksToUse = jose_1.createRemoteJWKSet( - new URL( - `${stInstance.appInfo.apiDomain.getAsStringDangerous()}${stInstance.appInfo.apiBasePath.getAsStringDangerous()}/jwt/jwks.json` - ) - ); + const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + jwksToUse = combinedRemoteJWKSet_1.getCombinedJWKS(sessionRecipe.config); } try { if (accessToken) { diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index 544685fb1..1d0e9660a 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -11,10 +11,10 @@ import SessionRecipe from "./recipe/session/recipe"; import { availableTokenTransferMethods } from "./recipe/session/constants"; import { getToken } from "./recipe/session/cookieAndHeaders"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; -import { JWTPayload, createRemoteJWKSet } from "jose"; -import SuperTokens from "./supertokens"; +import { JWTPayload } from "jose"; import { HTTPMethod } from "./types"; import { getInfoFromAccessToken } from "./recipe/session/accessToken"; +import { getCombinedJWKS } from "./combinedRemoteJWKSet"; export type GetCookieFn = (req: T) => Record; @@ -209,12 +209,8 @@ export async function getSessionForSSR( let jwksToUse = jwks; if (!jwks) { - const stInstance = SuperTokens.getInstanceOrThrowError(); - jwksToUse = createRemoteJWKSet( - new URL( - `${stInstance.appInfo.apiDomain.getAsStringDangerous()}${stInstance.appInfo.apiBasePath.getAsStringDangerous()}/jwt/jwks.json` - ) - ); + const sessionRecipe = SessionRecipe.getInstanceOrThrowError(); + jwksToUse = getCombinedJWKS(sessionRecipe.config); } try { From 2d18d33c15bafa8da8b4e1ac8a71434390bdd840 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Fri, 4 Oct 2024 13:04:50 +0530 Subject: [PATCH 20/28] Reuse more functionality from custom framework in next --- lib/build/customFramework.d.ts | 14 +++-- lib/build/customFramework.js | 4 +- lib/build/nextjs.js | 72 ++----------------------- lib/ts/customFramework.ts | 20 ++++--- lib/ts/nextjs.ts | 98 +++++----------------------------- 5 files changed, 40 insertions(+), 168 deletions(-) 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; From 564d9c0583f59110dbee319c4fc19f48e5fefd66 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Fri, 4 Oct 2024 13:16:47 +0530 Subject: [PATCH 21/28] Add a comment about acceptin jwks as a parameter for getSessionForSSR --- lib/build/customFramework.js | 4 ++++ lib/ts/customFramework.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index e83316624..72515b298 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -174,6 +174,10 @@ exports.getSessionDetails = getSessionDetails; async function getSessionForSSR(request, jwks) { const accessToken = getAccessToken(request); const hasToken = !!accessToken; + // NOTE: We are accepting jwks as a parameter so that this function can + // be effectively tested. + // There's more details on why this is needed in the tests file where this + // function is being tested. let jwksToUse = jwks; if (!jwks) { const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index e32d840de..a51894568 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -207,6 +207,10 @@ export async function getSessionForSSR( const accessToken = getAccessToken(request); const hasToken = !!accessToken; + // NOTE: We are accepting jwks as a parameter so that this function can + // be effectively tested. + // There's more details on why this is needed in the tests file where this + // function is being tested. let jwksToUse = jwks; if (!jwks) { const sessionRecipe = SessionRecipe.getInstanceOrThrowError(); From f22b3e2d1fada8cee9573f8143edec10c0f92a14 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 4 Oct 2024 18:39:35 +0530 Subject: [PATCH 22/28] several fixes and refactoring --- lib/build/customFramework.d.ts | 61 ++++----- lib/build/customFramework.js | 171 ++++++++----------------- lib/build/nextjs.d.ts | 16 +-- lib/build/nextjs.js | 61 ++------- lib/ts/customFramework.ts | 224 ++++++++++++--------------------- lib/ts/nextjs.ts | 77 +++--------- test/with-typescript/index.ts | 4 +- 7 files changed, 187 insertions(+), 427 deletions(-) diff --git a/lib/build/customFramework.d.ts b/lib/build/customFramework.d.ts index 7da61beb0..d7abeb160 100644 --- a/lib/build/customFramework.d.ts +++ b/lib/build/customFramework.d.ts @@ -7,7 +7,6 @@ import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; import { JWTPayload } from "jose"; -export declare type GetCookieFn = (req: T) => Record; export interface ParsableRequest { url: string; method: string; @@ -15,28 +14,13 @@ export interface ParsableRequest { formData: () => Promise; json: () => Promise; } -export declare function createPreParsedRequest( - request: RequestType, - getCookieFn?: GetCookieFn -): PreParsedRequest; -export declare function getCookieFromRequest(request: ParsableRequest): Record; -export declare function getQueryFromRequest(request: ParsableRequest): Record; -export declare function getHandleCall( - res: typeof Response, - stMiddleware: any -): (req: T) => Promise; -export declare function handleAuthAPIRequest(CustomResponse: typeof Response): (req: Request) => Promise; -export declare function getSessionDetails( - preParsedRequest: PreParsedRequest, - options?: VerifySessionOptions, - userContext?: Record -): Promise<{ - session: SessionContainer | undefined; - hasToken: boolean; - hasInvalidClaims: boolean; - baseResponse: CollectingResponse; - response?: Response; -}>; +export declare function getCookieFromRequest( + request: RequestType +): Record; +export declare function getQueryFromRequest( + request: RequestType +): Record; +export declare function handleAuthAPIRequest(): (req: Request) => Promise; /** * A helper function to retrieve session details on the server side. * @@ -44,9 +28,15 @@ export declare function getSessionDetails( * because getSession can update the access token. These updated tokens would not be * propagated to the client side, as request interceptors do not run on the server side. */ -export declare function getSessionForSSR( - request: Request, - jwks?: any +export declare function getSessionForSSR( + request: RequestType +): Promise<{ + accessTokenPayload: JWTPayload | undefined; + hasToken: boolean; + error: Error | undefined; +}>; +export declare function getSessionForSSRUsingAccessToken( + accessToken: string | undefined ): Promise<{ accessTokenPayload: JWTPayload | undefined; hasToken: boolean; @@ -59,15 +49,12 @@ export declare function withSession< request: RequestType, handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, options?: VerifySessionOptions, - userContext?: Record, - getCookieFn?: GetCookieFn + userContext?: Record +): Promise; +export declare function withPreParsedRequestResponse< + RequestType extends ParsableRequest = Request, + ResponseType extends Response = Response +>( + req: RequestType, + handler: (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => Promise ): Promise; -export declare function addCookies( - baseResponse: CollectingResponse, - userResponse: UserResponseType -): UserResponseType; -export declare function handleError( - err: any, - baseRequest: PreParsedRequest, - baseResponse: CollectingResponse -): Promise; diff --git a/lib/build/customFramework.js b/lib/build/customFramework.js index 72515b298..25064135d 100644 --- a/lib/build/customFramework.js +++ b/lib/build/customFramework.js @@ -10,24 +10,22 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.handleError = exports.addCookies = exports.withSession = exports.getSessionForSSR = exports.getSessionDetails = exports.handleAuthAPIRequest = exports.getHandleCall = exports.getQueryFromRequest = exports.getCookieFromRequest = exports.createPreParsedRequest = void 0; +exports.withPreParsedRequestResponse = exports.withSession = exports.getSessionForSSRUsingAccessToken = exports.getSessionForSSR = exports.handleAuthAPIRequest = exports.getQueryFromRequest = exports.getCookieFromRequest = void 0; const cookie_1 = require("cookie"); const custom_1 = require("./framework/custom"); const session_1 = __importDefault(require("./recipe/session")); const recipe_1 = __importDefault(require("./recipe/session/recipe")); -const constants_1 = require("./recipe/session/constants"); -const cookieAndHeaders_1 = require("./recipe/session/cookieAndHeaders"); const jwt_1 = require("./recipe/session/jwt"); const accessToken_1 = require("./recipe/session/accessToken"); const combinedRemoteJWKSet_1 = require("./combinedRemoteJWKSet"); -function createPreParsedRequest(request, getCookieFn = getCookieFromRequest) { +function createPreParsedRequest(request) { /** * This helper function can take any `Request` type of object * and parse the details into an equivalent PreParsedRequest * that can be used with the custom framework helpers. */ return new custom_1.PreParsedRequest({ - cookies: getCookieFn(request), + cookies: getCookieFromRequest(request), url: request.url, method: request.method, query: getQueryFromRequest(request), @@ -40,7 +38,6 @@ function createPreParsedRequest(request, getCookieFn = getCookieFromRequest) { }, }); } -exports.createPreParsedRequest = createPreParsedRequest; function getCookieFromRequest(request) { /** * This function will extract the cookies from any `Request` @@ -75,95 +72,34 @@ exports.getQueryFromRequest = getQueryFromRequest; function getAccessToken(request) { return getCookieFromRequest(request)["sAccessToken"]; } -function getHandleCall(res, stMiddleware) { +function getHandleCall(stMiddleware) { return async function handleCall(req) { - const baseResponse = new custom_1.CollectingResponse(); - const { handled, error } = await stMiddleware(req, baseResponse); - if (error) { - throw error; - } - if (!handled) { - return new res("Not found", { status: 404 }); - } - for (const respCookie of baseResponse.cookies) { - baseResponse.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, - }) - ); - } - return new res(baseResponse.body, { - headers: baseResponse.headers, - status: baseResponse.statusCode, + return withPreParsedRequestResponse(req, async (baseRequest, baseResponse) => { + const { handled, error } = await stMiddleware(baseRequest, baseResponse); + if (error) { + throw error; + } + if (!handled) { + return new Response("Not found", { status: 404 }); + } + return new Response(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); }); }; } -exports.getHandleCall = getHandleCall; -function handleAuthAPIRequest(CustomResponse) { +function handleAuthAPIRequest() { /** * Util function to handle all calls by intercepting them, calling * Supertokens middleware and then accordingly returning. */ const stMiddleware = custom_1.middleware((req) => { - return createPreParsedRequest(req); + return req; }); - return getHandleCall(CustomResponse, stMiddleware); + return getHandleCall(stMiddleware); } exports.handleAuthAPIRequest = handleAuthAPIRequest; -async function getSessionDetails(preParsedRequest, options, userContext) { - const baseResponse = new custom_1.CollectingResponse(); - // Possible interop issue. - const recipe = recipe_1.default.getInstanceOrThrowError(); - const tokenTransferMethod = recipe.config.getTokenTransferMethod({ - req: preParsedRequest, - forCreateNewSession: false, - userContext: userContext, - }); - const transferMethods = - tokenTransferMethod === "any" ? constants_1.availableTokenTransferMethods : [tokenTransferMethod]; - const hasToken = transferMethods.some((transferMethod) => { - const token = cookieAndHeaders_1.getToken(preParsedRequest, "access", transferMethod); - if (!token) { - return false; - } - try { - jwt_1.parseJWTWithoutSignatureVerification(token); - return true; - } catch (_a) { - return false; - } - }); - try { - const session = await session_1.default.getSession(preParsedRequest, baseResponse, options, userContext); - return { - session, - hasInvalidClaims: false, - hasToken, - baseResponse, - }; - } catch (err) { - if (session_1.default.Error.isErrorFromSuperTokens(err)) { - return { - hasToken, - hasInvalidClaims: err.type === session_1.default.Error.INVALID_CLAIMS, - session: undefined, - baseResponse, - response: new Response("Authentication required", { - status: err.type === session_1.default.Error.INVALID_CLAIMS ? 403 : 401, - }), - }; - } else { - throw err; - } - } -} -exports.getSessionDetails = getSessionDetails; /** * A helper function to retrieve session details on the server side. * @@ -171,52 +107,53 @@ exports.getSessionDetails = getSessionDetails; * because getSession can update the access token. These updated tokens would not be * propagated to the client side, as request interceptors do not run on the server side. */ -async function getSessionForSSR(request, jwks) { - const accessToken = getAccessToken(request); +async function getSessionForSSR(request) { + return getSessionForSSRUsingAccessToken(getAccessToken(request)); +} +exports.getSessionForSSR = getSessionForSSR; +async function getSessionForSSRUsingAccessToken(accessToken) { const hasToken = !!accessToken; - // NOTE: We are accepting jwks as a parameter so that this function can - // be effectively tested. - // There's more details on why this is needed in the tests file where this - // function is being tested. - let jwksToUse = jwks; - if (!jwks) { - const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); - jwksToUse = combinedRemoteJWKSet_1.getCombinedJWKS(sessionRecipe.config); - } try { - if (accessToken) { - const tokenInfo = jwt_1.parseJWTWithoutSignatureVerification(accessToken); - const decoded = await accessToken_1.getInfoFromAccessToken(tokenInfo, jwksToUse, false); - return { accessTokenPayload: decoded.userData, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - if (error instanceof Error && error.name === "JWTExpired") { + const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + const jwksToUse = combinedRemoteJWKSet_1.getCombinedJWKS(sessionRecipe.config); + try { + if (accessToken) { + const tokenInfo = jwt_1.parseJWTWithoutSignatureVerification(accessToken); + const decoded = await accessToken_1.getInfoFromAccessToken(tokenInfo, jwksToUse, false); + return { accessTokenPayload: decoded.userData, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } catch (error) { return { accessTokenPayload: undefined, hasToken, error: undefined }; } + } catch (error) { return { accessTokenPayload: undefined, hasToken, error: error }; } } -exports.getSessionForSSR = getSessionForSSR; -async function withSession(request, handler, options, userContext, getCookieFn = getCookieFromRequest) { +exports.getSessionForSSRUsingAccessToken = getSessionForSSRUsingAccessToken; +async function withSession(request, handler, options, userContext) { try { - const baseRequest = createPreParsedRequest(request, getCookieFn); - const { session, response, baseResponse } = await getSessionDetails(baseRequest, options, userContext); - if (response !== undefined) { - return response; - } - let userResponse; - try { - userResponse = await handler(undefined, session); - } catch (err) { - userResponse = await handleError(err, baseRequest, baseResponse); - } - return addCookies(baseResponse, userResponse); + return await withPreParsedRequestResponse(request, async (baseRequest, baseResponse) => { + const session = await session_1.default.getSession(baseRequest, baseResponse, options, userContext); + return handler(undefined, session); + }); } catch (error) { return await handler(error, undefined); } } exports.withSession = withSession; +async function withPreParsedRequestResponse(req, handler) { + let baseRequest = createPreParsedRequest(req); + let baseResponse = new custom_1.CollectingResponse(); + let userResponse; + try { + userResponse = await handler(baseRequest, baseResponse); + } catch (err) { + userResponse = await handleError(err, baseRequest, baseResponse); + } + return addCookies(baseResponse, userResponse); +} +exports.withPreParsedRequestResponse = withPreParsedRequestResponse; function addCookies(baseResponse, userResponse) { /** * Add cookies to the userResponse passed by copying it from the baseResponse. @@ -257,7 +194,6 @@ function addCookies(baseResponse, userResponse) { } return userResponse; } -exports.addCookies = addCookies; async function handleError(err, baseRequest, baseResponse) { await custom_1.errorHandler()(err, baseRequest, baseResponse, (errorHandlerError) => { if (errorHandlerError) { @@ -270,4 +206,3 @@ async function handleError(err, baseRequest, baseResponse) { headers: baseResponse.headers, }); } -exports.handleError = handleError; diff --git a/lib/build/nextjs.d.ts b/lib/build/nextjs.d.ts index 79ba7a644..18adb76ac 100644 --- a/lib/build/nextjs.d.ts +++ b/lib/build/nextjs.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; -import { GetCookieFn } from "./customFramework"; +import { JWTPayload } from "jose"; declare type PartialNextRequest = { method: string; url: string; @@ -21,22 +21,16 @@ export default class NextJS { request: any, response: any ): Promise; - static getCookieExtractor(): GetCookieFn; - static getAppDirRequestHandler( - NextResponse: typeof Response - ): (req: T) => Promise; + static getAppDirRequestHandler(): (req: Request) => Promise; static getSSRSession( cookies: Array<{ name: string; value: string; - }>, - headers: Headers, - options?: VerifySessionOptions, - userContext?: Record + }> ): Promise<{ - session: SessionContainer | undefined; + accessTokenPayload: JWTPayload | undefined; hasToken: boolean; - hasInvalidClaims: boolean; + error: Error | undefined; }>; static withSession( req: NextRequest, diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index 019006d3d..6ced5eff5 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -13,22 +13,9 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __rest = - (this && this.__rest) || - function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; - } - return t; - }; Object.defineProperty(exports, "__esModule", { value: true }); exports.withPreParsedRequestResponse = exports.withSession = exports.getSSRSession = exports.getAppDirRequestHandler = exports.superTokensNextWrapper = void 0; const express_1 = require("./framework/express"); -const utils_1 = require("./utils"); -const custom_1 = require("./framework/custom"); const customFramework_1 = require("./customFramework"); function next(request, response, resolve, reject) { return async function (middlewareError) { @@ -65,48 +52,22 @@ class NextJS { } }); } - static getCookieExtractor() { - return (req) => Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); - } - static getAppDirRequestHandler(NextResponse) { - const getCookieFromNextReq = NextJS.getCookieExtractor(); - const stMiddleware = custom_1.middleware((req) => { - return customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); - }); - return customFramework_1.getHandleCall(NextResponse, stMiddleware); + static getAppDirRequestHandler() { + return customFramework_1.handleAuthAPIRequest(); } - static async getSSRSession(cookies, headers, options, userContext) { - // 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: Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value])), - getFormBody: async () => [], - getJSONBody: async () => [], - }); - const _a = await customFramework_1.getSessionDetails(baseRequest, options, utils_1.getUserContext(userContext)), - { baseResponse, response } = _a, - result = __rest(_a, ["baseResponse", "response"]); - return result; + static async getSSRSession(cookies) { + var _a; + let accessToken = + (_a = cookies.find((cookie) => cookie.name === "sAccessToken")) === null || _a === void 0 + ? void 0 + : _a.value; + return await customFramework_1.getSessionForSSRUsingAccessToken(accessToken); } static async withSession(req, handler, options, userContext) { - const getCookieFromNextReq = NextJS.getCookieExtractor(); - return await customFramework_1.withSession(req, handler, options, userContext, getCookieFromNextReq); + return await customFramework_1.withSession(req, handler, options, userContext); } static async withPreParsedRequestResponse(req, handler) { - const getCookieFromNextReq = NextJS.getCookieExtractor(); - let baseRequest = customFramework_1.createPreParsedRequest(req, getCookieFromNextReq); - let baseResponse = new custom_1.CollectingResponse(); - let userResponse; - try { - userResponse = await handler(baseRequest, baseResponse); - } catch (err) { - userResponse = await customFramework_1.handleError(err, baseRequest, baseResponse); - } - return customFramework_1.addCookies(baseResponse, userResponse); + return customFramework_1.withPreParsedRequestResponse(req, handler); } } exports.default = NextJS; diff --git a/lib/ts/customFramework.ts b/lib/ts/customFramework.ts index a51894568..36c273d26 100644 --- a/lib/ts/customFramework.ts +++ b/lib/ts/customFramework.ts @@ -8,15 +8,12 @@ import { serialize } from "cookie"; import { CollectingResponse, errorHandler, middleware, PreParsedRequest } from "./framework/custom"; import Session, { SessionContainer, VerifySessionOptions } from "./recipe/session"; import SessionRecipe from "./recipe/session/recipe"; -import { availableTokenTransferMethods } from "./recipe/session/constants"; -import { getToken } from "./recipe/session/cookieAndHeaders"; import { parseJWTWithoutSignatureVerification } from "./recipe/session/jwt"; import { JWTPayload } from "jose"; import { HTTPMethod } from "./types"; import { getInfoFromAccessToken } from "./recipe/session/accessToken"; import { getCombinedJWKS } from "./combinedRemoteJWKSet"; - -export type GetCookieFn = (req: T) => Record; +import { BaseRequest } from "./framework"; export interface ParsableRequest { url: string; @@ -26,17 +23,14 @@ export interface ParsableRequest { json: () => Promise; } -export function createPreParsedRequest( - request: RequestType, - getCookieFn: GetCookieFn = getCookieFromRequest -): PreParsedRequest { +function createPreParsedRequest(request: RequestType): PreParsedRequest { /** * This helper function can take any `Request` type of object * and parse the details into an equivalent PreParsedRequest * that can be used with the custom framework helpers. */ return new PreParsedRequest({ - cookies: getCookieFn(request), + cookies: getCookieFromRequest(request), url: request.url as string, method: request.method as HTTPMethod, query: getQueryFromRequest(request), @@ -50,7 +44,9 @@ export function createPreParsedRequest { +export function getCookieFromRequest( + request: RequestType +): Record { /** * This function will extract the cookies from any `Request` * type of object and return them to be usable with PreParsedRequest. @@ -67,7 +63,9 @@ export function getCookieFromRequest(request: ParsableRequest): Record { +export function getQueryFromRequest( + request: RequestType +): Record { /** * Helper function to extract query from any `Request` type of * object and return them to be usable with PreParsedRequest. @@ -81,112 +79,38 @@ export function getQueryFromRequest(request: ParsableRequest): Record(request: RequestType): string | undefined { return getCookieFromRequest(request)["sAccessToken"]; } -export function getHandleCall(res: typeof Response, stMiddleware: any) { - return async function handleCall(req: T) { - const baseResponse = new CollectingResponse(); - - const { handled, error } = await stMiddleware(req, baseResponse); - - if (error) { - throw error; - } - if (!handled) { - return new res("Not found", { status: 404 }); - } - - for (const respCookie of baseResponse.cookies) { - baseResponse.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, - }) - ); - } - - return new res(baseResponse.body, { - headers: baseResponse.headers, - status: baseResponse.statusCode, +function getHandleCall(stMiddleware: any) { + return async function handleCall(req: RequestType) { + return withPreParsedRequestResponse(req, async (baseRequest, baseResponse) => { + const { handled, error } = await stMiddleware(baseRequest, baseResponse); + if (error) { + throw error; + } + if (!handled) { + return new Response("Not found", { status: 404 }); + } + return new Response(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); }); }; } -export function handleAuthAPIRequest(CustomResponse: typeof Response) { +export function handleAuthAPIRequest() { /** * Util function to handle all calls by intercepting them, calling * Supertokens middleware and then accordingly returning. */ - const stMiddleware = middleware((req) => { - return createPreParsedRequest(req); - }); - - return getHandleCall(CustomResponse, stMiddleware); -} - -export async function getSessionDetails( - preParsedRequest: PreParsedRequest, - options?: VerifySessionOptions, - userContext?: Record -): Promise<{ - session: SessionContainer | undefined; - hasToken: boolean; - hasInvalidClaims: boolean; - baseResponse: CollectingResponse; - response?: Response; -}> { - const baseResponse = new CollectingResponse(); - // Possible interop issue. - const recipe = SessionRecipe.getInstanceOrThrowError(); - const tokenTransferMethod = recipe.config.getTokenTransferMethod({ - req: preParsedRequest, - forCreateNewSession: false, - userContext: userContext as any, - }); - const transferMethods = tokenTransferMethod === "any" ? availableTokenTransferMethods : [tokenTransferMethod]; - const hasToken = transferMethods.some((transferMethod) => { - const token = getToken(preParsedRequest, "access", transferMethod); - if (!token) { - return false; - } - try { - parseJWTWithoutSignatureVerification(token); - return true; - } catch { - return false; - } + const stMiddleware = middleware((req) => { + return req; }); - try { - const session = await Session.getSession(preParsedRequest, baseResponse, options, userContext); - return { - session, - hasInvalidClaims: false, - hasToken, - baseResponse, - }; - } catch (err) { - if (Session.Error.isErrorFromSuperTokens(err)) { - return { - hasToken, - hasInvalidClaims: err.type === Session.Error.INVALID_CLAIMS, - session: undefined, - baseResponse, - response: new Response("Authentication required", { - status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401, - }), - }; - } else { - throw err; - } - } + return getHandleCall(stMiddleware); } /** @@ -196,38 +120,39 @@ export async function getSessionDetails( * because getSession can update the access token. These updated tokens would not be * propagated to the client side, as request interceptors do not run on the server side. */ -export async function getSessionForSSR( - request: Request, - jwks?: any +export async function getSessionForSSR( + request: RequestType ): Promise<{ accessTokenPayload: JWTPayload | undefined; hasToken: boolean; error: Error | undefined; }> { - const accessToken = getAccessToken(request); - const hasToken = !!accessToken; + return getSessionForSSRUsingAccessToken(getAccessToken(request)); +} - // NOTE: We are accepting jwks as a parameter so that this function can - // be effectively tested. - // There's more details on why this is needed in the tests file where this - // function is being tested. - let jwksToUse = jwks; - if (!jwks) { +export async function getSessionForSSRUsingAccessToken( + accessToken: string | undefined +): Promise<{ + accessTokenPayload: JWTPayload | undefined; + hasToken: boolean; + error: Error | undefined; +}> { + const hasToken = !!accessToken; + try { const sessionRecipe = SessionRecipe.getInstanceOrThrowError(); - jwksToUse = getCombinedJWKS(sessionRecipe.config); - } + const jwksToUse = getCombinedJWKS(sessionRecipe.config); - try { - if (accessToken) { - const tokenInfo = parseJWTWithoutSignatureVerification(accessToken); - const decoded = await getInfoFromAccessToken(tokenInfo, jwksToUse, false); - return { accessTokenPayload: decoded.userData, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - if (error instanceof Error && error.name === "JWTExpired") { + try { + if (accessToken) { + const tokenInfo = parseJWTWithoutSignatureVerification(accessToken); + const decoded = await getInfoFromAccessToken(tokenInfo, jwksToUse, false); + return { accessTokenPayload: decoded.userData, hasToken, error: undefined }; + } + return { accessTokenPayload: undefined, hasToken, error: undefined }; + } catch (error) { return { accessTokenPayload: undefined, hasToken, error: undefined }; } + } catch (error) { return { accessTokenPayload: undefined, hasToken, error: error as Error }; } } @@ -239,32 +164,39 @@ export async function withSession< request: RequestType, handler: (error: Error | undefined, session: SessionContainer | undefined) => Promise, options?: VerifySessionOptions, - userContext?: Record, - getCookieFn: GetCookieFn = getCookieFromRequest + userContext?: Record ): Promise { try { - const baseRequest = createPreParsedRequest(request, getCookieFn); - const { session, response, baseResponse } = await getSessionDetails(baseRequest, options, userContext); - - if (response !== undefined) { - return response as ResponseType; - } - - let userResponse: Response; - - try { - userResponse = await handler(undefined, session); - } catch (err) { - userResponse = await handleError(err, baseRequest, baseResponse); - } - - return addCookies(baseResponse, userResponse) as ResponseType; + return await withPreParsedRequestResponse(request, async (baseRequest, baseResponse) => { + const session = await Session.getSession(baseRequest, baseResponse, options, userContext); + return handler(undefined, session); + }); } catch (error) { return await handler(error as Error, undefined); } } -export function addCookies( +export async function withPreParsedRequestResponse< + RequestType extends ParsableRequest = Request, + ResponseType extends Response = Response +>( + req: RequestType, + handler: (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => Promise +): Promise { + let baseRequest = createPreParsedRequest(req); + let baseResponse = new CollectingResponse(); + let userResponse: ResponseType; + + try { + userResponse = await handler(baseRequest, baseResponse); + } catch (err) { + userResponse = await handleError(err, baseRequest, baseResponse); + } + + return addCookies(baseResponse, userResponse); +} + +function addCookies( baseResponse: CollectingResponse, userResponse: UserResponseType ): UserResponseType { @@ -311,7 +243,7 @@ export function addCookies( return userResponse; } -export async function handleError( +async function handleError( err: any, baseRequest: PreParsedRequest, baseResponse: CollectingResponse diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index 38fe2d3ca..1688175e8 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -14,17 +14,14 @@ */ import { errorHandler } from "./framework/express"; -import { getUserContext } from "./utils"; -import { CollectingResponse, PreParsedRequest, middleware } from "./framework/custom"; +import { CollectingResponse, PreParsedRequest } from "./framework/custom"; import { SessionContainer, VerifySessionOptions } from "./recipe/session"; +import { JWTPayload } from "jose"; import { - addCookies, - createPreParsedRequest, - GetCookieFn, - getHandleCall, - getSessionDetails, - handleError, + withPreParsedRequestResponse as customWithPreParsedRequestResponse, + getSessionForSSRUsingAccessToken, withSession as customWithSession, + handleAuthAPIRequest, } from "./customFramework"; function next( @@ -88,47 +85,19 @@ export default class NextJS { }); } - static getCookieExtractor(): GetCookieFn { - return (req: T): Record => - Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); - } - - static getAppDirRequestHandler(NextResponse: typeof Response) { - const getCookieFromNextReq = NextJS.getCookieExtractor(); - const stMiddleware = middleware((req) => { - return createPreParsedRequest(req, getCookieFromNextReq); - }); - return getHandleCall(NextResponse, stMiddleware); + static getAppDirRequestHandler() { + return handleAuthAPIRequest(); } static async getSSRSession( - cookies: Array<{ name: string; value: string }>, - headers: Headers, - options?: VerifySessionOptions, - userContext?: Record + cookies: Array<{ name: string; value: string }> ): Promise<{ - session: SessionContainer | undefined; + accessTokenPayload: JWTPayload | undefined; hasToken: boolean; - hasInvalidClaims: boolean; + error: Error | undefined; }> { - // 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: Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value])), - getFormBody: async () => [], - getJSONBody: async () => [], - }); - - const { baseResponse, response, ...result } = await getSessionDetails( - baseRequest, - options, - getUserContext(userContext) - ); - return result; + let accessToken = cookies.find((cookie) => cookie.name === "sAccessToken")?.value; + return await getSessionForSSRUsingAccessToken(accessToken); } static async withSession( @@ -137,32 +106,14 @@ export default class NextJS { options?: VerifySessionOptions, userContext?: Record ): Promise { - const getCookieFromNextReq = NextJS.getCookieExtractor(); - return await customWithSession( - req, - handler, - options, - userContext, - getCookieFromNextReq - ); + return await customWithSession(req, handler, options, userContext); } static async withPreParsedRequestResponse( req: NextRequest, handler: (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => Promise ): Promise { - const getCookieFromNextReq = NextJS.getCookieExtractor(); - let baseRequest = createPreParsedRequest(req, getCookieFromNextReq); - let baseResponse = new CollectingResponse(); - let userResponse: NextResponse; - - try { - userResponse = await handler(baseRequest, baseResponse); - } catch (err) { - userResponse = await handleError(err, baseRequest, baseResponse); - } - - return addCookies(baseResponse, userResponse); + return customWithPreParsedRequestResponse(req, handler); } } export let superTokensNextWrapper = NextJS.superTokensNextWrapper; diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index b9be6ec15..97dc2dfbf 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -2258,13 +2258,13 @@ async function handleCall(req: NextRequest): Promise { }); } -NextJS.getAppDirRequestHandler(NextResponse); +NextJS.getAppDirRequestHandler(); customVerifySession({ checkDatabase: true })(new PreParsedRequest({} as any), new CollectingResponse()); const nextRequest = new NextRequest("http://localhost:3000/api/user"); -NextJS.getSSRSession(nextRequest.cookies.getAll(), nextRequest.headers); +NextJS.getSSRSession(nextRequest.cookies.getAll()); NextJS.withSession(nextRequest, async function test(session): Promise { return NextResponse.json({}); }); From 892c05b77e271366865f54820dd4593f06edabad Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Mon, 7 Oct 2024 08:01:41 +0530 Subject: [PATCH 23/28] Fix custom framework related tests --- test/customFramework.test.js | 98 ++---------------------------------- 1 file changed, 5 insertions(+), 93 deletions(-) diff --git a/test/customFramework.test.js b/test/customFramework.test.js index aee2a5ab2..b78966e99 100644 --- a/test/customFramework.test.js +++ b/test/customFramework.test.js @@ -65,60 +65,6 @@ async function signJWT(privateKey, jwks, payload, expiresIn = "2h") { .sign(privateKey); } -describe(`createPreParsedRequest ${printPath("[test/customFramework.test.js]")}`, () => { - it("should create a PreParsedRequest with correct properties from the Request object", async () => { - // Mock a Request object - const mockRequest = { - url: "https://example.com/path?name=test", - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - Authorization: "Bearer token", - Cookie: "session=abcd1234; theme=dark", - }), - formData: async () => new FormData(), - json: async () => ({ key: "value" }), - }; - - // Assume getCookieFromRequest and getQueryFromRequest return specific mock data - const mockCookies = { session: "abcd1234", theme: "dark" }; - const mockQuery = { name: "test" }; - - // Create the PreParsedRequest - const preParsedReq = createPreParsedRequest(mockRequest); - - // Assertions - assert(preParsedReq instanceof PreParsedRequest, "Should return an instance of PreParsedRequest"); - assert.deepStrictEqual( - preParsedReq.getCookieValue("session"), - mockCookies.session, - "Should parse `session` value from cookie correctly" - ); - assert.deepStrictEqual( - preParsedReq.getCookieValue("theme"), - mockCookies.theme, - "Should parse `session` value from cookie correctly" - ); - assert.strictEqual(preParsedReq.getOriginalURL(), mockRequest.url, "Should set the correct URL"); - assert.strictEqual(preParsedReq.getMethod(), mockRequest.method.toLowerCase(), "Should set the correct method"); - assert.deepStrictEqual( - preParsedReq.getKeyValueFromQuery("name"), - mockQuery.name, - "Should parse query parameters correctly" - ); - assert.strictEqual( - preParsedReq.getHeaderValue("Authorization"), - mockRequest.headers.get("Authorization"), - "Should set the correct headers" - ); - - // Test getJSONBody methods - const jsonBody = await preParsedReq.getJSONBody(); - - assert.deepStrictEqual(jsonBody, { key: "value" }, "getJSONBody should return parsed JSON body"); - }); -}); - describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, () => { let connectionURI; let accessToken, accessTokenPayload; @@ -349,22 +295,14 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, assert.strictEqual(await response.text(), "Not found", "Should return Not found"); }); - // NOTE: For all the JWT related testing, we are using a different key because - // the default way of getting the key is by hitting the `/jwt/jwks` endpoint - // but that endpoint doesn't return anything for testing and thus we are testing - // with a custom key. - it("getSessionForSSR should return session for valid token", async () => { - // Sign the JWT - const validToken = await signJWT(privateKey, jwks, accessTokenPayload); - // Create a mock request containing the valid token as a cookie const mockRequest = new Request("https://example.com", { - headers: { Cookie: `sAccessToken=${validToken}` }, + headers: { Cookie: `sAccessToken=${accessToken}` }, }); // Call the getSessionForSSR function - const result = await getSessionForSSR(mockRequest, await createJWTVerifyGetKey(jwks)); + const result = await getSessionForSSR(mockRequest); // Assertions assert.strictEqual(result.hasToken, true, "hasToken should be true for a valid token"); @@ -378,7 +316,7 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, const mockRequest = new Request("https://example.com"); // Call the getSessionForSSR function - const result = await getSessionForSSR(mockRequest, privateKey); + const result = await getSessionForSSR(mockRequest); // Assertions assert.strictEqual(result.hasToken, false, "hasToken should be false when no token is present"); @@ -390,32 +328,6 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, assert.strictEqual(result.error, undefined, "error should be undefined when no token is present"); }); - it("should handle an expired token gracefully", async () => { - // Sign the JWT with an expiration time in the past (e.g., 1 second ago) - const expiredToken = await signJWT(privateKey, jwks, accessTokenPayload, Math.floor(Date.now() / 1000) - 1); - - // Create a mock request containing the expired token as a cookie - const mockRequest = new Request("https://example.com", { - headers: { Cookie: `sAccessToken=${expiredToken}` }, - }); - - // Call the getSessionForSSR function - const result = await getSessionForSSR(mockRequest, privateKey); - - // Assertions - assert.strictEqual(result.hasToken, true, "hasToken should be true for an expired token"); - assert.strictEqual( - result.accessTokenPayload, - undefined, - "accessTokenPayload should be undefined for an expired token" - ); - assert.strictEqual( - result.error.type, - "TRY_REFRESH_TOKEN", - "error should be TRY_REFRESH_TOKEN for an expired token" - ); - }); - it("should return an error for an invalid token", async () => { // Assume you have an invalid token that does not match the JWKS const invalidToken = "your-invalid-jwt-token"; @@ -426,7 +338,7 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, }); // Call the getSessionForSSR function - const result = await getSessionForSSR(mockRequest, privateKey); + const result = await getSessionForSSR(mockRequest); // Assertions assert.strictEqual(result.hasToken, true, "hasToken should be true for an invalid token"); @@ -435,6 +347,6 @@ describe(`handleAuthAPIRequest ${printPath("[test/customFramework.test.js]")}`, undefined, "accessTokenPayload should be undefined for an invalid token" ); - assert.ok(result.error instanceof Error, "error should be an instance of Error for an invalid token"); + assert.strictEqual(result.error, undefined, "error should be undefined for an invalid token"); }); }); From 1d6e6791b8a926cd283b86a0a8e4efceaf21ee7e Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Mon, 7 Oct 2024 10:50:41 +0530 Subject: [PATCH 24/28] Add/fix tests for next js --- test/nextjs.test.js | 256 +++++++++++++++++++------------------------- 1 file changed, 113 insertions(+), 143 deletions(-) diff --git a/test/nextjs.test.js b/test/nextjs.test.js index 3e7744988..35d5cdcd5 100644 --- a/test/nextjs.test.js +++ b/test/nextjs.test.js @@ -13,10 +13,12 @@ * under the License. */ +const { parseJWTWithoutSignatureVerification } = require("../lib/build/recipe/session/jwt"); + const [major, minor, patch] = process.versions.node.split(".").map(Number); if (major >= 18) { - const { printPath, setupST, startST, killAllST, cleanST, delay } = require("./utils"); + const { printPath, setupST, startST, killAllST, cleanST, delay, killAllSTCoresOnly } = require("./utils"); let assert = require("assert"); let { ProcessState } = require("../lib/build/processState"); let SuperTokens = require("../lib/build/").default; @@ -729,58 +731,30 @@ if (major >= 18) { const authenticatedRequest = new NextRequest("http://localhost:3000/api/get-user", { headers: { - Authorization: `Bearer ${tokens.access}`, + Cookie: `sAccessToken=${tokens.access}`, }, }); - let sessionContainer = await getSSRSession( - authenticatedRequest.cookies.getAll(), - authenticatedRequest.headers - ); + let sessionContainer = await getSSRSession(authenticatedRequest.cookies.getAll()); assert.equal(sessionContainer.hasToken, true); - assert.equal(sessionContainer.session.getUserId(), process.env.user); + assert.equal(sessionContainer.accessTokenPayload.sub, process.env.user); const unAuthenticatedRequest = new NextRequest("http://localhost:3000/api/get-user"); - sessionContainer = await getSSRSession( - unAuthenticatedRequest.cookies.getAll(), - unAuthenticatedRequest.headers - ); + sessionContainer = await getSSRSession(unAuthenticatedRequest.cookies.getAll()); assert.equal(sessionContainer.hasToken, false); - assert.equal(sessionContainer.session, undefined); - - const requestWithFailedClaim = new NextRequest("http://localhost:3000/api/get-user", { - headers: { - Authorization: `Bearer ${tokens.access}`, - }, - }); - - sessionContainer = await getSSRSession( - requestWithFailedClaim.cookies.getAll(), - requestWithFailedClaim.headers, - { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - EmailVerification.EmailVerificationClaim.validators.isVerified(), - ], - } - ); - assert.equal(sessionContainer.hasToken, true); - assert.equal(sessionContainer.hasInvalidClaims, true); + assert.equal(sessionContainer.accessTokenPayload, undefined); await delay(3); const requestWithExpiredToken = new NextRequest("http://localhost:3000/api/get-user", { headers: { - Authorization: `Bearer ${tokens.access}`, + Cookie: `sAccessToken=${tokens.access}`, }, }); - sessionContainer = await getSSRSession( - requestWithExpiredToken.cookies.getAll(), - requestWithExpiredToken.headers - ); + sessionContainer = await getSSRSession(requestWithExpiredToken.cookies.getAll()); assert.equal(sessionContainer.session, undefined); assert.equal(sessionContainer.hasToken, true); }); @@ -841,7 +815,7 @@ if (major >= 18) { const requestWithFailedClaim = new NextRequest("http://localhost:3000/api/get-user", { headers: { - Authorization: `Bearer ${tokens.access}`, + Cookie: `sAccessToken=${tokens.access}`, }, }); @@ -893,6 +867,42 @@ if (major >= 18) { assert.equal(responseThatThrows.status, 500); }); + it("withSession with updated access token payload should be correctly returned", async () => { + const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "header" }); + + const authenticatedRequest = new NextRequest("http://localhost:3000/api/get-user", { + headers: { + Authorization: `Bearer ${tokens.access}`, + }, + }); + + const authenticatedResponse = await withSession(authenticatedRequest, async (err, session) => { + if (err) return NextResponse.json(err, { status: 500 }); + + // Update token payload + await session.mergeIntoAccessTokenPayload({ test: true }); + + return NextResponse.json({ + userId: session.getUserId(), + sessionHandle: session.getHandle(), + accessTokenPayload: session.getAccessTokenPayload(), + }); + }); + const updatedAccessToken = authenticatedResponse.headers.get("st-access-token"); + const tokenInfo = parseJWTWithoutSignatureVerification(updatedAccessToken); + + assert.strictEqual( + authenticatedResponse.headers.get("Cache-Control"), + "no-cache, no-store, max-age=0, must-revalidate", + "cache control headers should be set" + ); + assert.strictEqual( + tokenInfo.payload.test, + true, + "access token payload should have a test value that is true" + ); + }); + it("withPreParsedRequestResponse", async function () { const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "header" }); @@ -977,32 +987,17 @@ if (major >= 18) { const requestWithNoToken = new NextRequest("http://localhost:3000/api/get-user"); - sessionContainer = await getSSRSession(requestWithNoToken.cookies.getAll(), requestWithNoToken.headers); + sessionContainer = await getSSRSession(requestWithNoToken.cookies.getAll()); assert.equal(sessionContainer.hasToken, false); const requestWithInvalidToken = new NextRequest("http://localhost:3000/api/get-user", { headers: { - Authorization: `Bearer some-random-token`, - }, - }); - - sessionContainer = await getSSRSession( - requestWithInvalidToken.cookies.getAll(), - requestWithInvalidToken.headers - ); - assert.equal(sessionContainer.hasToken, false); - - const requestWithTokenInHeader = new NextRequest("http://localhost:3000/api/get-user", { - headers: { - Authorization: `Bearer ${tokens.access}`, + Cookie: `sAccessToken=some-random-token`, }, }); - sessionContainer = await getSSRSession( - requestWithTokenInHeader.cookies.getAll(), - requestWithTokenInHeader.headers - ); + sessionContainer = await getSSRSession(requestWithInvalidToken.cookies.getAll()); assert.equal(sessionContainer.hasToken, true); const requestWithTokenInCookie = new NextRequest("http://localhost:3000/api/get-user", { @@ -1011,10 +1006,7 @@ if (major >= 18) { }, }); - sessionContainer = await getSSRSession( - requestWithTokenInCookie.cookies.getAll(), - requestWithTokenInCookie.headers - ); + sessionContainer = await getSSRSession(requestWithTokenInCookie.cookies.getAll()); assert.equal(sessionContainer.hasToken, true); }); }); @@ -1055,7 +1047,7 @@ if (major >= 18) { const requestWithNoToken = new NextRequest("http://localhost:3000/api/get-user"); - sessionContainer = await getSSRSession(requestWithNoToken.cookies.getAll(), requestWithNoToken.headers); + sessionContainer = await getSSRSession(requestWithNoToken.cookies.getAll()); assert.equal(sessionContainer.hasToken, false); @@ -1065,11 +1057,8 @@ if (major >= 18) { }, }); - sessionContainer = await getSSRSession( - requestWithInvalidToken.cookies.getAll(), - requestWithInvalidToken.headers - ); - assert.equal(sessionContainer.hasToken, false); + sessionContainer = await getSSRSession(requestWithInvalidToken.cookies.getAll()); + assert.equal(sessionContainer.hasToken, true); const requestWithTokenInHeader = new NextRequest("http://localhost:3000/api/get-user", { headers: { @@ -1077,10 +1066,7 @@ if (major >= 18) { }, }); - sessionContainer = await getSSRSession( - requestWithTokenInHeader.cookies.getAll(), - requestWithTokenInHeader.headers - ); + sessionContainer = await getSSRSession(requestWithTokenInHeader.cookies.getAll()); assert.equal(sessionContainer.hasToken, false); const requestWithTokenInCookie = new NextRequest("http://localhost:3000/api/get-user", { @@ -1089,90 +1075,74 @@ if (major >= 18) { }, }); - sessionContainer = await getSSRSession( - requestWithTokenInCookie.cookies.getAll(), - requestWithTokenInCookie.headers - ); + sessionContainer = await getSSRSession(requestWithTokenInCookie.cookies.getAll()); assert.equal(sessionContainer.hasToken, true); }); }); + }); - describe("tokenTransferMethod = header", function () { - before(async function () { - process.env.user = undefined; - await killAllST(); - await setupST(); - const connectionURI = await startST(); - ProcessState.getInstance().reset(); - SuperTokens.init({ - supertokens: { - connectionURI, - }, - appInfo: { - apiDomain: "api.supertokens.io", - appName: "SuperTokens", - apiBasePath: "/api/auth", - websiteDomain: "supertokens.io", - }, - recipeList: [ - EmailPassword.init(), - Session.init({ - getTokenTransferMethod: () => "header", - }), - ], - }); - }); - - after(async function () { - await killAllST(); - await cleanST(); + describe("with email verification should throw st-ev claim has expired", async () => { + before(async function () { + process.env.user = undefined; + await killAllST(); + await setupST(); + const connectionURI = await startST(); + ProcessState.getInstance().reset(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + apiBasePath: "/api/auth", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init({ + getTokenTransferMethod: () => "any", + }), + EmailVerification.init({ + mode: "REQUIRED", + }), + ], }); + }); - it("should return hasToken value correctly", async function () { - const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "header" }); - - const requestWithNoToken = new NextRequest("http://localhost:3000/api/get-user"); - - sessionContainer = await getSSRSession(requestWithNoToken.cookies.getAll(), requestWithNoToken.headers); - - assert.equal(sessionContainer.hasToken, false); - - const requestWithInvalidToken = new NextRequest("http://localhost:3000/api/get-user", { - headers: { - Authorization: `Bearer some-random-token`, - }, - }); - - sessionContainer = await getSSRSession( - requestWithInvalidToken.cookies.getAll(), - requestWithInvalidToken.headers - ); - assert.equal(sessionContainer.hasToken, false); - - const requestWithTokenInHeader = new NextRequest("http://localhost:3000/api/get-user", { - headers: { - Authorization: `Bearer ${tokens.access}`, - }, - }); + after(async function () { + await killAllST(); + await cleanST(); + }); - sessionContainer = await getSSRSession( - requestWithTokenInHeader.cookies.getAll(), - requestWithTokenInHeader.headers - ); - assert.equal(sessionContainer.hasToken, true); + it("should throw st-ev claim has expired for unverified email", async () => { + const tokens = await getValidTokensAfterSignup(); + const authenticatedRequest = new NextRequest("http://localhost:3000/api/get-user", { + headers: { + Cookie: `sAccessToken=${tokens.access}`, + }, + }); - const requestWithTokenInCookie = new NextRequest("http://localhost:3000/api/get-user", { - headers: { - Cookie: `sAccessToken=${tokens.access}`, - }, + const authenticatedResponse = await withSession(authenticatedRequest, async (err, session) => { + if (err) return NextResponse.json(`CUSTOM_ERROR: ${err}`, { status: 500 }); + return NextResponse.json({ + userId: session.getUserId(), + sessionHandle: session.getHandle(), + accessTokenPayload: session.getAccessTokenPayload(), }); - - sessionContainer = await getSSRSession( - requestWithTokenInCookie.cookies.getAll(), - requestWithTokenInCookie.headers - ); - assert.equal(sessionContainer.hasToken, false); }); + const responseJSON = await authenticatedResponse.json(); + assert.strictEqual(responseJSON.message, "invalid claim", "should return message: invalid claim"); + assert.strictEqual( + responseJSON.claimValidationErrors.length, + 1, + "should return claim validation errors of length 1" + ); + assert.strictEqual( + responseJSON.claimValidationErrors[0].id, + "st-ev", + "should return claim validation error id as st-ev" + ); }); }); From 57be23d72837d086130d7f7de9af385488c9fb2b Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Mon, 7 Oct 2024 12:10:03 +0530 Subject: [PATCH 25/28] Add test for session refresh in next --- .../app/api/auth/[...path]/route.ts | 4 +- test/nextjs.test.js | 92 ++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/examples/next/with-emailpassword/app/api/auth/[...path]/route.ts b/examples/next/with-emailpassword/app/api/auth/[...path]/route.ts index 4385f250f..51385b9db 100644 --- a/examples/next/with-emailpassword/app/api/auth/[...path]/route.ts +++ b/examples/next/with-emailpassword/app/api/auth/[...path]/route.ts @@ -1,11 +1,11 @@ import { getAppDirRequestHandler } from "supertokens-node/nextjs"; -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import supertokens from "supertokens-node"; import { backendConfig } from "../../../../config/backendConfig"; supertokens.init(backendConfig()); -const handleCall = getAppDirRequestHandler(NextResponse); +const handleCall = getAppDirRequestHandler(); export async function GET(request: NextRequest) { const res = await handleCall(request); diff --git a/test/nextjs.test.js b/test/nextjs.test.js index 35d5cdcd5..6883fc35f 100644 --- a/test/nextjs.test.js +++ b/test/nextjs.test.js @@ -18,7 +18,16 @@ const { parseJWTWithoutSignatureVerification } = require("../lib/build/recipe/se const [major, minor, patch] = process.versions.node.split(".").map(Number); if (major >= 18) { - const { printPath, setupST, startST, killAllST, cleanST, delay, killAllSTCoresOnly } = require("./utils"); + const { + printPath, + setupST, + startST, + killAllST, + cleanST, + delay, + killAllSTCoresOnly, + extractInfoFromResponse, + } = require("./utils"); let assert = require("assert"); let { ProcessState } = require("../lib/build/processState"); let SuperTokens = require("../lib/build/").default; @@ -948,6 +957,85 @@ if (major >= 18) { assert.strictEqual(error, unknownError); } }); + + // it("should go to next error handler when withSession is called without core", async function () { + // const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "header" }); + + // const authenticatedRequest = new NextRequest("http://localhost:3000/api/get-user", { + // headers: { + // Cookie: `sAccessToken=${tokens.access}`, + // }, + // }); + + // // Manually kill to get error when withSession is called + // await killAllSTCoresOnly(); + + // const authenticatedResponse = await withSession(authenticatedRequest, async (err, session) => { + // if (err) return NextResponse.json(`CUSTOM_ERROR: ${err}`, { status: 500 }); + // return NextResponse.json({ + // userId: session.getUserId(), + // sessionHandle: session.getHandle(), + // accessTokenPayload: session.getAccessTokenPayload(), + // }); + // }); + // const responseJSON = await authenticatedResponse.json(); + // console.log(responseJSON); + // assert.strictEqual(responseJSON, {}, "test"); + // }); + }); + + describe("session refresh test", async () => { + before(async function () { + process.env.user = undefined; + await killAllST(); + await setupST(); + const connectionURI = await startST(); + ProcessState.getInstance().reset(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + apiBasePath: "/api/auth", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Session.init({ + getTokenTransferMethod: () => "cookie", + }), + ], + }); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("should successfully refresh session", async () => { + const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "cookie" }); + + const authenticatedRequest = new NextRequest("http://localhost:3000/api/auth/session/refresh", { + headers: { + Cookie: `sAccessToken=${tokens.access};sRefreshToken=${tokens.refresh}`, + "st-auth-mode": "cookie", + }, + }); + + const authenticatedResponse = await withPreParsedRequestResponse( + authenticatedRequest, + async (baseRequest, baseResponse) => { + const session = await Session.getSession(baseRequest, baseResponse); + return NextResponse.json({ userId: session.getUserId() }); + } + ); + const responseJSON = await authenticatedResponse.json(); + assert.equal(authenticatedResponse.status, 200, "response should return a 200 OK"); + assert.ok(responseJSON.userId, "response should contain the user ID"); + }); }); describe(`getSSRSession:hasToken`, function () { @@ -1199,6 +1287,8 @@ if (major >= 18) { } } + tokens.antiCsrf = response.headers.get("anti-csrf"); + return tokens; } } From bc8fa9a8fdc57721113e3ec02c6c3b64fed4572a Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Mon, 7 Oct 2024 13:29:57 +0530 Subject: [PATCH 26/28] Add test for core being down and error handler called in next --- test/nextjs.test.js | 55 +++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/test/nextjs.test.js b/test/nextjs.test.js index 6883fc35f..ac334555d 100644 --- a/test/nextjs.test.js +++ b/test/nextjs.test.js @@ -958,30 +958,37 @@ if (major >= 18) { } }); - // it("should go to next error handler when withSession is called without core", async function () { - // const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "header" }); - - // const authenticatedRequest = new NextRequest("http://localhost:3000/api/get-user", { - // headers: { - // Cookie: `sAccessToken=${tokens.access}`, - // }, - // }); - - // // Manually kill to get error when withSession is called - // await killAllSTCoresOnly(); - - // const authenticatedResponse = await withSession(authenticatedRequest, async (err, session) => { - // if (err) return NextResponse.json(`CUSTOM_ERROR: ${err}`, { status: 500 }); - // return NextResponse.json({ - // userId: session.getUserId(), - // sessionHandle: session.getHandle(), - // accessTokenPayload: session.getAccessTokenPayload(), - // }); - // }); - // const responseJSON = await authenticatedResponse.json(); - // console.log(responseJSON); - // assert.strictEqual(responseJSON, {}, "test"); - // }); + it("should go to next error handler when withSession is called without core", async function () { + const tokens = await getValidTokensAfterSignup({ tokenTransferMethod: "header" }); + + const authenticatedRequest = new NextRequest("http://localhost:3000/api/get-user", { + headers: { + Cookie: `sAccessToken=${tokens.access}`, + }, + }); + + // Manually kill to get error when withSession is called + await killAllSTCoresOnly(); + + const authenticatedResponse = await withSession( + authenticatedRequest, + async (err, session) => { + if (err) return NextResponse.json(`CUSTOM_ERROR: ${err}`, { status: 500 }); + return NextResponse.json({ + userId: session.getUserId(), + sessionHandle: session.getHandle(), + accessTokenPayload: session.getAccessTokenPayload(), + }); + }, + { checkDatabase: true } + ); + const responseJSON = await authenticatedResponse.json(); + assert.strictEqual( + responseJSON, + "CUSTOM_ERROR: Error: No SuperTokens core available to query", + "should return custom error from next error handler" + ); + }); }); describe("session refresh test", async () => { From 8deff185568e4056276982310c95f771c595b182 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Mon, 7 Oct 2024 13:33:30 +0530 Subject: [PATCH 27/28] Add detail of breaking change in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd1e4581..359c6a2b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - This means that we removed `override.openIdFeature` from the Session recipe configuration - Removed `getJWKS` from the OpenId recipe, as it is already exposed by the JWT recipe - We now automatically initialize the OpenId and JWT recipes even if you do not use the Session recipe +- `getAppDirRequestHandler` for `nextjs` will no longer accept a Response object. ### Migration From 75c4e28efa56a7b863ca3181a952ebead65ba637 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Mon, 7 Oct 2024 13:35:58 +0530 Subject: [PATCH 28/28] Adds migration guide for using updated next.js handler function --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 359c6a2b3..fe275c657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,25 @@ SuperTokens.init({ }); ``` +#### Using updated `getAppDirRequestHandler` for next.js + +Before: + +```ts +import { getAppDirRequestHandler } from "supertokens-node/nextjs"; +import { NextResponse } from "next/server"; + +const handleCall = getAppDirRequestHandler(NextResponse); +``` + +After: + +```ts +import { getAppDirRequestHandler } from "supertokens-node/nextjs"; + +const handleCall = getAppDirRequestHandler(); +``` + ## [20.1.3] - 2024-09-30 - Replaces `psl` with `tldts` to avoid `punycode` deprecation warning.