UNPKG

@kilterset/auth0-actions-testing

Version:

Test and develop Auth0 Actions or Okta CIC Actions locally.

294 lines (249 loc) 8.09 kB
import test from "node:test"; import { redirectMock } from "../../mock/api/redirect"; import { request as mockRequest, user as mockUser } from "../../mock"; import { deepStrictEqual, ok, strictEqual, throws } from "node:assert"; import { encodeHS256JWT } from "../../jwt"; test("redirect", async (t) => { const baseApi = Symbol("Base API"); await t.test("encodeToken", async (t) => { const now = new Date("2024-01-01T00:00:00.000Z"); const nowUnixTimestamp = now.getTime() / 1000; const request = mockRequest({ hostname: "example.com", ip: "d666:171e:e7a4:1aa3:359a:a317:9f53:ee97", }); const user = mockUser({ user_id: "auth0|8150" }); const { build, state } = redirectMock("Another Flow", { now, request, user, }); const api = build(baseApi); const token = api.encodeToken({ expiresInSeconds: 42, payload: { foo: "bar" }, secret: "shh", }); const [header, payload, signature] = token.split("."); const decodedHeader = JSON.parse(atob(header)); const decodedPayload = JSON.parse(atob(payload)); deepStrictEqual( decodedHeader, { alg: "HS256", typ: "JWT" }, "unexpected JWT header" ); deepStrictEqual( decodedPayload, { iss: "example.com", iat: nowUnixTimestamp, exp: nowUnixTimestamp + 42, sub: "auth0|8150", ip: "d666:171e:e7a4:1aa3:359a:a317:9f53:ee97", foo: "bar", }, "unexpected claims" ); strictEqual( signature, "ZlLKLk7uJzDjD0nt2a08QiWMY1EPnhFIuc8WsSZPBvQ", "invalid signature" ); }); await t.test("sendUserTo", async (t) => { await t.test("simple redirect", async (t) => { const now = new Date(); const request = mockRequest(); const user = mockUser(); const { build, state } = redirectMock("Another Flow", { now, request, user, }); const api = build(baseApi); strictEqual(api.sendUserTo("https://example.com/r"), baseApi); ok(state.target, "redirect not set"); deepStrictEqual(state.target.queryParams, {}, "query should be empty"); strictEqual( state.target.url.href, "https://example.com/r", "url mismatch" ); }); await t.test("redirect with consolidated GET parameters", async (t) => { const now = new Date(); const request = mockRequest(); const user = mockUser(); const { build, state } = redirectMock("Another Flow", { now, request, user, }); const api = build(baseApi); strictEqual( api.sendUserTo("https://example.com?bread=rye", { query: { filling: "cheese", spread: "butter" }, }), baseApi ); ok(state.target, "redirect not set"); deepStrictEqual( state.target.queryParams, { bread: "rye", filling: "cheese", spread: "butter" }, "unexpected query" ); strictEqual( state.target.url.href, "https://example.com/?bread=rye&filling=cheese&spread=butter", "url mismatch" ); }); }); await t.test("validateToken", async (t) => { const VALID_SECRET = "shh"; const TOKEN_PAYLOAD = { sub: "auth0|7321", iss: "myapp.com", iat: 1711509300, exp: 1711509800, state: "opaque-random-state", foo: "bar", }; const VALID_STATE = TOKEN_PAYLOAD.state; const VALID_TOKEN = encodeHS256JWT({ secret: VALID_SECRET, claims: TOKEN_PAYLOAD, }); const VALID_CONTEXT = { now: TOKEN_PAYLOAD.iat * 1000, user: mockUser({ user_id: TOKEN_PAYLOAD.sub }), request: mockRequest({ method: "POST", body: { session_token: VALID_TOKEN, state: VALID_STATE, }, }), } as const; await t.test("decodes a valid POST token", async (t) => { const { now, user } = VALID_CONTEXT; const request = mockRequest({ method: "POST", body: { session_token: VALID_TOKEN, state: VALID_STATE, }, }); const { build, state } = redirectMock("Another Flow", { now, request, user, }); const api = build(baseApi); const payload = api.validateToken({ secret: VALID_SECRET }); deepStrictEqual(payload, TOKEN_PAYLOAD, "unexpected payload"); }); await t.test("decodes a valid GET token", async (t) => { const { now, user } = VALID_CONTEXT; const request = mockRequest({ method: "GET", query: { session_token: VALID_TOKEN, state: VALID_STATE, }, }); const { build, state } = redirectMock("Another Flow", { now, request, user, }); const api = build(baseApi); const payload = api.validateToken({ secret: VALID_SECRET }); deepStrictEqual(payload, TOKEN_PAYLOAD, "unexpected payload"); }); await t.test("can use an alternative parameter", async (t) => { const { now, user } = VALID_CONTEXT; const request = mockRequest({ method: "POST", body: { a_token_param: VALID_TOKEN, state: VALID_STATE, }, }); const { build, state } = redirectMock("Another Flow", { now, request, user, }); const api = build(baseApi); const payload = api.validateToken({ secret: VALID_SECRET, tokenParameterName: "a_token_param", }); deepStrictEqual(payload, TOKEN_PAYLOAD, "unexpected payload"); }); await t.test("throws error if state is mismatched", async (t) => { const context = structuredClone(VALID_CONTEXT); context.request.body.state = "mismatched-from-token-value"; const { build, state } = redirectMock("Another Flow", context); const api = build(baseApi); throws( () => api.validateToken({ secret: VALID_SECRET }), /state in the token/i ); }); await t.test("throws error if param is missing", async (t) => { const { build, state } = redirectMock("Another Flow", VALID_CONTEXT); const api = build(baseApi); throws( () => api.validateToken({ secret: VALID_SECRET, tokenParameterName: "a_token_param", }), /no parameter called 'a_token_param'/i ); }); await t.test("throws error if expired", async (t) => { const { user, request } = VALID_CONTEXT; const now = TOKEN_PAYLOAD.exp * 1000; // exactly the expiry time const { build, state } = redirectMock("Another Flow", { ...VALID_CONTEXT, now, }); const api = build(baseApi); throws(() => api.validateToken({ secret: VALID_SECRET }), /expired/i); }); await t.test("throws error if signature is invalid", async (t) => { const context = structuredClone(VALID_CONTEXT); context.request.body.session_token = VALID_TOKEN.replace( /\.[^.]+$/, ".badsignature" ); const { build, state } = redirectMock("Another Flow", context); const api = build(baseApi); throws(() => api.validateToken({ secret: VALID_SECRET }), /signature/i); }); for (const claim of ["sub", "iss", "iat", "exp"]) { await t.test(`throws error if ${claim} is missing`, async (t) => { const context = structuredClone(VALID_CONTEXT); const claims = structuredClone(TOKEN_PAYLOAD) as Record< string, unknown >; ok(claim in claims, "claim not in payload"); delete claims[claim]; context.request.body.session_token = encodeHS256JWT({ secret: VALID_SECRET, claims, }); const { build, state } = redirectMock("Another Flow", context); const api = build(baseApi); throws( () => api.validateToken({ secret: VALID_SECRET }), /missing or invalid standard claims/i ); }); } }); });