UNPKG

@kilterset/auth0-actions-testing

Version:

Test and develop Auth0 Actions or Okta CIC Actions locally.

134 lines (103 loc) 3.47 kB
import { ok } from "assert"; import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; import { Request, User } from "../../types"; interface RedirectMockOptions { readonly now?: ConstructorParameters<typeof Date>[0]; readonly request: Request; readonly user: User; } interface EncodeTokenOptions { expiresInSeconds?: number | undefined; payload: { [key: string]: unknown; }; secret: string; } interface SendUserToOptions { query?: Record<string, string>; } interface ValidateTokenOptions { tokenParameterName?: string; secret: string; } export function redirectMock( flow: string, { now: nowValue, request, user }: RedirectMockOptions ) { const now = new Date(nowValue || Date.now()); const state = { target: null as null | { url: URL; queryParams: Record<string, string> }, }; const build = <T>(api: T) => ({ encodeToken: ({ expiresInSeconds, payload, secret, }: EncodeTokenOptions) => { expiresInSeconds = expiresInSeconds ?? 900; const claims = { iss: request.hostname, iat: Math.floor(now.getTime() / 1000), exp: Math.floor((now.getTime() + expiresInSeconds * 1000) / 1000), sub: user.user_id, ip: request.ip, ...payload, }; return encodeHS256JWT({ secret, claims }); }, sendUserTo: (urlString: string, options?: SendUserToOptions) => { const url = new URL(urlString); if (options?.query) { for (const [key, value] of Object.entries(options.query)) { url.searchParams.append(key, value); } } const queryParams = Object.fromEntries(url.searchParams.entries()); state.target = { url, queryParams }; return api; }, validateToken: ({ tokenParameterName, secret }: ValidateTokenOptions) => { tokenParameterName = tokenParameterName ?? "session_token"; const params = { ...request.query, ...request.body }; const tokenValue = params[tokenParameterName]; ok( tokenParameterName in params, `There is no parameter called '${tokenParameterName}' available in either the POST body or query string.` ); const [rawHeader, rawClaims, signature] = String(tokenValue).split("."); const verify = (condition: boolean, message: string) => { ok(condition, `The session token is invalid: ${message}`); }; const [header, claims] = [rawHeader, rawClaims].map((part) => JSON.parse(Buffer.from(part, "base64url").toString()) ); verify( claims.state === params.state, "State in the token does not match the /continue state." ); const expectedSignature = signHS256({ secret, body: `${rawHeader}.${rawClaims}`, }); verify(signature === expectedSignature, "Failed signature validation"); const expectedClaims = ["sub", "iss", "exp", "iat"]; for (const claim of expectedClaims) { verify(claim in claims, "Missing or invalid standard claims"); } verify( header.typ?.toUpperCase() === "JWT", "Unexpected token payload type" ); verify( claims.sub === user.user_id, "The sub claim does not match the user_id." ); verify( claims.exp > Math.floor(now.getTime() / 1000), "Token has expired." ); return claims as Record<string, unknown>; }, }); return { state, build }; }