diff --git a/test/test-server/src/index.ts b/test/test-server/src/index.ts index 7787e0d15..0eaa50a56 100644 --- a/test/test-server/src/index.ts +++ b/test/test-server/src/index.ts @@ -336,6 +336,7 @@ app.get("/test/featureflag", async (req, res, next) => { app.post("/test/resetoverrideparams", async (req, res, next) => { resetOverrideParams(); + resetOverrideLogs(); res.json({ ok: true }); }); diff --git a/test/test-server/src/session.ts b/test/test-server/src/session.ts index 838f13038..a6f6ac9c3 100644 --- a/test/test-server/src/session.ts +++ b/test/test-server/src/session.ts @@ -1,10 +1,11 @@ import { Router } from "express"; import Session from "../../../recipe/session"; import * as supertokens from "../../../lib/build"; -import { PrimitiveClaim } from "../../../lib/build/recipe/session/claims"; import SessionRecipe from "../../../lib/build/recipe/session/recipe"; import { logger } from "./logger"; import { getFunc } from "./testFunctionMapper"; +import { convertRequestSessionToSessionObject, deserializeClaim, deserializeValidator } from "./utils"; +import { logOverrideEvent } from "./overrideLogging"; const namespace = "com.supertokens:node-test-server:session"; const { logDebugMessage } = logger(namespace); @@ -110,10 +111,7 @@ const router = Router() .post("/fetchandsetclaim", async (req, res, next) => { try { logDebugMessage("Session.fetchAndSetClaim %j", req.body); - let claim = new PrimitiveClaim({ - key: req.body.claim.key, - fetchValue: getFunc(`${req.body.claim.fetchValue}`), - }); + let claim = deserializeClaim(req.body.claim); const response = await Session.fetchAndSetClaim(req.body.sessionHandle, claim, req.body.userContext); res.json(response); } catch (e) { @@ -147,6 +145,328 @@ const router = Router() } catch (e) { next(e); } + }) + .post("/sessionobject/revokesession", async (req, res, next) => { + logDebugMessage("Session.sessionobject.revokesession %j", req.body); + logOverrideEvent("sessionobject.revokesession", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.revokeSession(req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.revokesession", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.revokesession", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getsessiondatafromdatabase", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getsessiondatafromdatabase %j", req.body); + logOverrideEvent("sessionobject.getsessiondatafromdatabase", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getSessionDataFromDatabase(req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getsessiondatafromdatabase", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getsessiondatafromdatabase", "REJ", e); + next(e); + } + }) + .post("/sessionobject/updatesessiondataindatabase", async (req, res, next) => { + logDebugMessage("Session.sessionobject.updatesessiondataindatabase %j", req.body); + logOverrideEvent("sessionobject.updatesessiondataindatabase", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.updateSessionDataInDatabase(req.body.newSessionData, req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.updatesessiondataindatabase", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.updatesessiondataindatabase", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getuserid", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getuserid %j", req.body); + logOverrideEvent("sessionobject.getuserid", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getUserId(req.body.userContext); // : string; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getuserid", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getuserid", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getrecipeuserid", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getrecipeuserid %j", req.body); + logOverrideEvent("sessionobject.getrecipeuserid", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getRecipeUserId(req.body.userContext); // : RecipeUserId; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getrecipeuserid", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getrecipeuserid", "REJ", e); + next(e); + } + }) + .post("/sessionobject/gettenantid", async (req, res, next) => { + logDebugMessage("Session.sessionobject.gettenantid %j", req.body); + logOverrideEvent("sessionobject.gettenantid", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getTenantId(req.body.userContext); // : string; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.gettenantid", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.gettenantid", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getaccesstokenpayload", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getaccesstokenpayload %j", req.body); + logOverrideEvent("sessionobject.getaccesstokenpayload", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getAccessTokenPayload(req.body.userContext); // : any; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getaccesstokenpayload", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getaccesstokenpayload", "REJ", e); + next(e); + } + }) + .post("/sessionobject/gethandle", async (req, res, next) => { + logDebugMessage("Session.sessionobject.gethandle %j", req.body); + logOverrideEvent("sessionobject.gethandle", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getHandle(req.body.userContext); // : string; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.gethandle", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.gethandle", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getallsessiontokensdangerously", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getallsessiontokensdangerously %j", req.body); + logOverrideEvent("sessionobject.getallsessiontokensdangerously", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getAllSessionTokensDangerously(); // : Promise<{}>; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getallsessiontokensdangerously", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getallsessiontokensdangerously", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getaccesstoken", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getaccesstoken %j", req.body); + logOverrideEvent("sessionobject.getaccesstoken", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getAccessToken(req.body.userContext); // : string; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getaccesstoken", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getaccesstoken", "REJ", e); + next(e); + } + }) + .post("/sessionobject/mergeintoaccesstokenpayload", async (req, res, next) => { + logDebugMessage("Session.sessionobject.mergeintoaccesstokenpayload %j", req.body); + logOverrideEvent("sessionobject.mergeintoaccesstokenpayload", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.mergeIntoAccessTokenPayload( + req.body.accessTokenPayloadUpdate, + req.body.userContext + ); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.mergeintoaccesstokenpayload", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.mergeintoaccesstokenpayload", "REJ", e); + next(e); + } + }) + .post("/sessionobject/gettimecreated", async (req, res, next) => { + logDebugMessage("Session.sessionobject.gettimecreated %j", req.body); + logOverrideEvent("sessionobject.gettimecreated", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getTimeCreated(req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.gettimecreated", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.gettimecreated", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getexpiry", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getexpiry %j", req.body); + logOverrideEvent("sessionobject.getexpiry", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getExpiry(req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getexpiry", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getexpiry", "REJ", e); + next(e); + } + }) + .post("/sessionobject/assertclaims", async (req, res, next) => { + logDebugMessage("Session.sessionobject.assertclaims %j", req.body); + logOverrideEvent("sessionobject.assertclaims", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.assertClaims( + req.body.claimValidators.map(deserializeValidator), + req.body.userContext + ); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.assertclaims", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.assertclaims", "REJ", e); + next(e); + } + }) + .post("/sessionobject/fetchandsetclaim", async (req, res, next) => { + logDebugMessage("Session.sessionobject.fetchandsetclaim %j", req.body); + logOverrideEvent("sessionobject.fetchandsetclaim", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + + const retVal = await session.fetchAndSetClaim(deserializeClaim(req.body.claim), req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.fetchandsetclaim", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.fetchandsetclaim", "REJ", e); + next(e); + } + }) + .post("/sessionobject/setclaimvalue", async (req, res, next) => { + logDebugMessage("Session.sessionobject.setclaimvalue %j", req.body); + logOverrideEvent("sessionobject.setclaimvalue", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.setClaimValue( + deserializeClaim(req.body.claim), + req.body.value, + req.body.userContext + ); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.setclaimvalue", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.setclaimvalue", "REJ", e); + next(e); + } + }) + .post("/sessionobject/removeclaim", async (req, res, next) => { + logDebugMessage("Session.sessionobject.removeClaim %j", req.body); + logOverrideEvent("sessionobject.removeClaim", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + await session.removeClaim(deserializeClaim(req.body.claim), req.body.userContext); // : Promise; + res.json({ updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.removeClaim", "RES", undefined); + } catch (e) { + logOverrideEvent("sessionobject.removeClaim", "REJ", e); + next(e); + } + }) + .post("/sessionobject/getclaimvalue", async (req, res, next) => { + logDebugMessage("Session.sessionobject.getclaimvalue %j", req.body); + logOverrideEvent("sessionobject.getclaimvalue", "CALL", req.body); + try { + const session = await convertRequestSessionToSessionObject(req.body.session); + if (!session) { + throw new Error("This should never happen: failed to deserialize session"); + } + const retVal = await session.getClaimValue(deserializeClaim(req.body.claim), req.body.userContext); // : Promise; + res.json({ retVal, updatedSession: { ...session } }); + + logOverrideEvent("sessionobject.getclaimvalue", "RES", retVal); + } catch (e) { + logOverrideEvent("sessionobject.getclaimvalue", "REJ", e); + next(e); + } + }) + .post("/sessionobject/attachtorequestresponse", async (req, res, next) => { + logDebugMessage("Session.sessionobject.attachtorequestresponse %j", req.body); + logOverrideEvent("sessionobject.attachtorequestresponse", "CALL", req.body); + throw new Error("This should never happen: attachToRequestResponse called on remote-test session obj"); }); export default router; diff --git a/test/test-server/src/utils.ts b/test/test-server/src/utils.ts index 96c28f4e0..69ecb21ce 100644 --- a/test/test-server/src/utils.ts +++ b/test/test-server/src/utils.ts @@ -1,19 +1,140 @@ import Session from "../../../recipe/session"; import * as supertokens from "../../../lib/build"; +import { parseJWTWithoutSignatureVerification } from "../../../lib/build/recipe/session/jwt"; +import SessionClass from "../../../lib/build/recipe/session/sessionClass"; +import SessionRecipe from "../../../lib/build/recipe/session/recipe"; +import { SessionClaimValidator, TokenInfo } from "../../../lib/build/recipe/session/types"; +import { BooleanClaim, PrimitiveArrayClaim, PrimitiveClaim } from "../../../lib/build/recipe/session/claims"; +import { EmailVerificationClaim } from "../../../recipe/emailverification"; +import { MultiFactorAuthClaim } from "../../../recipe/multifactorauth"; +import { PermissionClaim, UserRoleClaim } from "../../../recipe/userroles"; +import { logOverrideEvent } from "./overrideLogging"; + +const testClaimSetups = { + "st-true": () => + new BooleanClaim({ + key: "st-true", + fetchValue: () => true, + }), + "st-undef": () => + new BooleanClaim({ + key: "st-undef", + fetchValue: () => undefined, + }), +}; + +// Add all built-in claims +for (const c of [EmailVerificationClaim, MultiFactorAuthClaim, UserRoleClaim, PermissionClaim]) { + testClaimSetups[c.key] = () => c; +} + +const mockClaimBuilder = ({ key, values }) => { + const claim = new PrimitiveClaim({ + key: key ?? "st-stub-primitive", + fetchValue: (userId, recipeUserId, tenantId, currentPayload, userContext) => { + logOverrideEvent(`claim-${key}.fetchValue`, "CALL", { + userId, + recipeUserId, + tenantId, + currentPayload, + userContext, + }); + + // Here we can't reuse popOrUseVal because the values are arrays themselves + const retVal = + userContext["st-stub-arr-value"] ?? + (values instanceof Array && values[0] instanceof Array ? values.pop() : values); + logOverrideEvent(`claim-${key}.fetchValue`, "RES", retVal); + + return retVal; + }, + }); + + return claim; +}; + +export function deserializeClaim(serializedClaim: { key: string; values: any }) { + if (serializedClaim.key.startsWith("st-stub-")) { + return mockClaimBuilder({ ...serializedClaim, key: serializedClaim.key.replace(/^st-stub-/, "") }); + } + return testClaimSetups[serializedClaim.key](serializedClaim); +} + +export function deserializeValidator( + serializedValidator: { key: string } & ( + | { validatorName: string; args: any[] } + | { id?: string; shouldRefetchRes: boolean | boolean[]; validateRes: any | any[] } + ) +): SessionClaimValidator { + const claim = testClaimSetups[serializedValidator.key](serializedValidator); + if ("validatorName" in serializedValidator) { + return claim.validators[serializedValidator.validatorName](...serializedValidator.args); + } + return { + id: serializedValidator.id ?? serializedValidator.key, + claim, + shouldRefetch: (payload, ctx) => { + logOverrideEvent(`${serializedValidator.key}-shouldRefetch`, "CALL", { payload, ctx }); + const retVal = + ctx[`${serializedValidator.key}-shouldRefetch-res`] ?? + popOrUseVal(serializedValidator.shouldRefetchRes); + logOverrideEvent(`${serializedValidator.key}-shouldRefetch`, "RES", { retVal }); + + return retVal; + }, + validate: (payload, ctx) => { + logOverrideEvent(`${serializedValidator.key}-validate`, "CALL", { payload, ctx }); + const retVal = + ctx[`${serializedValidator.key}-validate-res`] ?? popOrUseVal(serializedValidator.validateRes); + logOverrideEvent(`${serializedValidator.key}-validate`, "RES", { retVal }); + return retVal; + }, + }; +} export async function convertRequestSessionToSessionObject( - session: { [key: string]: any } | undefined + tokens: + | { + accessToken: string; + frontToken: string; + refreshToken: TokenInfo | undefined; + antiCsrfToken: string | undefined; + } + | undefined ): Promise { - if (session !== undefined) { - return await Session.getSessionWithoutRequestResponse( - session.accessToken, - session.userDataInAccessToken?.antiCsrfToken, - { - overrideGlobalClaimValidators: () => [], - } + if (tokens !== undefined) { + const helpers = { + config: SessionRecipe.getInstanceOrThrowError().config, + getRecipeImpl: () => SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl, + }; + + const jwtInfo = parseJWTWithoutSignatureVerification(tokens.accessToken); + const jwtPayload = jwtInfo.payload; + + let userId = jwtInfo.version === 2 ? jwtPayload.userId! : jwtPayload.sub!; + let sessionHandle = jwtPayload.sessionHandle!; + + let recipeUserId = new supertokens.RecipeUserId(jwtPayload.rsub ?? userId); + let antiCsrfToken = jwtPayload.antiCsrfToken; + let tenantId = jwtInfo.version >= 4 ? jwtPayload.tId! : "public"; + + const session = new SessionClass( + helpers as any, + tokens.accessToken, + tokens.frontToken, + tokens.refreshToken, + antiCsrfToken, + sessionHandle, + userId, + recipeUserId, + jwtPayload, + undefined, + false, + tenantId ); + return session; } - return session; + return tokens; } export function serializeUser(response) { @@ -35,3 +156,13 @@ export function serializeRecipeUserId(response) { : {}), }; } + +function popOrUseVal(arrOrValue: T | T[]): T { + if (arrOrValue instanceof Array) { + if (arrOrValue.length === 0) { + throw new Error("Ran out of values"); + } + return arrOrValue.pop()!; + } + return arrOrValue; +}