From 6b71b99577d5ce65d3032cb34ef91980aad3a6c5 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Wed, 10 Apr 2024 09:28:54 +1200 Subject: [PATCH 01/13] Extract access token api --- src/mock/api/access-token.ts | 73 ++++++++++++++++++++++++++++ src/mock/api/credentials-exchange.ts | 13 ++--- src/mock/api/post-login.ts | 30 ++---------- src/test/api/access-token.test.ts | 64 ++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 src/mock/api/access-token.ts create mode 100644 src/test/api/access-token.test.ts diff --git a/src/mock/api/access-token.ts b/src/mock/api/access-token.ts new file mode 100644 index 0000000..6ed1831 --- /dev/null +++ b/src/mock/api/access-token.ts @@ -0,0 +1,73 @@ +interface AccessTokenMock { + build: (api: T) => { + addScope: (name: string) => T; + removeScope: (name: string) => T; + setCustomClaim: (name: string, value: unknown) => T; + }; + state: { + scopes: string[]; + claims: Record; + }; +} + +interface CredentialsExchangeAccessTokenMock { + build: (api: T) => { + setCustomClaim: (name: string, value: unknown) => T; + }; + state: { + claims: Record; + }; +} + +export function accessTokenMock(flow: "CredentialsExchange"): CredentialsExchangeAccessTokenMock; +export function accessTokenMock(flow: string): AccessTokenMock; +export function accessTokenMock(flow: string) { + switch(flow) { + case "CredentialsExchange": { + const state = { + claims: {} as Record, + }; + + const build = (api: T) => ({ + setCustomClaim: (name: string, value: unknown) => { + state.claims[name] = value; + return api; + }, + }) + + return { build, state }; + } + + default: { + const state = { + scopes: [] as string[], + claims: {} as Record, + }; + + const build = (api: T) => ({ + addScope: (name: string) => { + state.scopes = [ + ...new Set(state.scopes).add(name), + ]; + + return api; + }, + + removeScope: (name: string) => { + state.scopes = state.scopes.filter( + (value) => value !== name + ); + + return api; + }, + + setCustomClaim: (name: string, value: unknown) => { + state.claims[name] = value; + return api; + }, + }) + + return { build, state }; + } + } +} diff --git a/src/mock/api/credentials-exchange.ts b/src/mock/api/credentials-exchange.ts index 11471ea..8852c00 100644 --- a/src/mock/api/credentials-exchange.ts +++ b/src/mock/api/credentials-exchange.ts @@ -1,6 +1,7 @@ import Auth0 from "../../types"; import { cache as mockCache } from "./cache"; import { request as mockRequest } from "../request"; +import { accessTokenMock } from "./access-token"; export interface CredentialsExchangeOptions { cache?: Record; @@ -18,12 +19,11 @@ export function credentialsExchange({ cache, }: CredentialsExchangeOptions = {}) { const apiCache = mockCache(cache); + const accessToken = accessTokenMock("CredentialsExchange") const state: CredentialsExchangeState = { access: { denied: false }, - accessToken: { - claims: {}, - }, + accessToken: accessToken.state, cache: apiCache, }; @@ -35,11 +35,8 @@ export function credentialsExchange({ }, }, - accessToken: { - setCustomClaim: (name, value) => { - state.accessToken.claims[name] = value; - return api; - }, + get accessToken() { + return accessToken.build(api); }, cache: apiCache, diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index 54615ec..8ce3632 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -4,6 +4,7 @@ import { user as mockUser } from "../user"; import { request as mockRequest } from "../request"; import { ok } from "node:assert"; import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; +import { accessTokenMock } from "./access-token"; export interface PostLoginOptions { user?: Auth0.User; @@ -88,6 +89,7 @@ export function postLogin({ now: nowValue, }: PostLoginOptions = {}) { const apiCache = mockCache(cache); + const accessToken = accessTokenMock("PostLogin"); const executedRules = optionallyExecutedRules ?? []; const userValue = user ?? mockUser(); const requestValue = request ?? mockRequest(); @@ -100,10 +102,7 @@ export function postLogin({ user: userValue, primaryUserId: userValue.user_id, access: { denied: false }, - accessToken: { - claims: {}, - scopes: [], - }, + accessToken: accessToken.state, authentication: { challenge: false, enrollment: false, @@ -185,27 +184,8 @@ export function postLogin({ }, }, - accessToken: { - addScope: (name) => { - state.accessToken.scopes = [ - ...new Set(state.accessToken.scopes).add(name), - ]; - - return api; - }, - - removeScope: (name) => { - state.accessToken.scopes = state.accessToken.scopes.filter( - (value) => value !== name - ); - - return api; - }, - - setCustomClaim: (name, value) => { - state.accessToken.claims[name] = value; - return api; - }, + get accessToken() { + return accessToken.build(api); }, authentication: { diff --git a/src/test/api/access-token.test.ts b/src/test/api/access-token.test.ts new file mode 100644 index 0000000..23b2cdb --- /dev/null +++ b/src/test/api/access-token.test.ts @@ -0,0 +1,64 @@ +import test from "node:test"; +import { accessTokenMock } from "../../mock/api/access-token"; +import { deepStrictEqual, ok, strictEqual } from "node:assert"; + +test("access token", async (t) => { + const baseApi = Symbol("baseApi"); + + await t.test("default state", () => { + const { state } = accessTokenMock("Another Flow"); + + deepStrictEqual(state, { + scopes: [], + claims: {}, + }); + }); + + await t.test("add scope", () => { + const { state, build } = accessTokenMock("Another Flow"); + const api = build(baseApi); + + strictEqual(api.addScope("read:users"), baseApi, "Expected base api to be returned"); + + ok(state.scopes.includes("read:users"), "Expected scope to be added"); + }); + + await t.test("remove scope", () => { + const { state, build } = accessTokenMock("Another Flow"); + const api = build(baseApi); + + api.addScope("read:users"); + + strictEqual(api.removeScope("read:users"), baseApi, "Expected base api to be returned"); + + ok(!state.scopes.includes("read:users"), "Expected scope to be removed"); + }); + + await t.test("set custom claim", () => { + const { state, build } = accessTokenMock("Another Flow"); + const api = build(baseApi); + + strictEqual(api.setCustomClaim("name", "Alice"), baseApi, "Expected base api to be returned"); + + deepStrictEqual(state.claims, { name: "Alice" }, "Expected custom claim to be set"); + }); + + await t.test("CredentialsExchange", async (t) => { + await t.test("default state", async (t) => { + const { state } = accessTokenMock("CredentialsExchange"); + + deepStrictEqual(state, { + claims: {}, + }); + }); + + await t.test("set custom claim", async (t) => { + const { state, build } = accessTokenMock("CredentialsExchange"); + const api = build(baseApi); + + strictEqual(api.setCustomClaim("name", "Alice"), baseApi, "Expected base api to be returned"); + + deepStrictEqual(state.claims, { name: "Alice" }, "Expected custom claim to be set"); + }); + }); +}); From 8ff15c21beec0916ca22462f9a60bfb190844c36 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Wed, 10 Apr 2024 13:02:23 +1200 Subject: [PATCH 02/13] Extract mock access api --- examples/geo-filter.test.js | 4 +-- src/mock/api/access.ts | 53 ++++++++++++++++++++++++++++ src/mock/api/credentials-exchange.ts | 13 ++++--- src/mock/api/post-login.ts | 13 ++++--- src/test/api/access.test.ts | 44 +++++++++++++++++++++++ src/test/api/post-login.test.ts | 6 ++-- 6 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 src/mock/api/access.ts create mode 100644 src/test/api/access.test.ts diff --git a/examples/geo-filter.test.js b/examples/geo-filter.test.js index 2028397..d5e57a0 100644 --- a/examples/geo-filter.test.js +++ b/examples/geo-filter.test.js @@ -16,13 +16,13 @@ test("Filter access based on continent code", async (t) => { ok(action.access.denied, "Expected access to be denied"); strictEqual( - action.access.code, + action.access.denied.code, "invalid_request", "Unexpected denial code" ); strictEqual( - action.access.reason, + action.access.denied.reason, "Access from North America is not allowed.", "Unexpected denial reason" ); diff --git a/src/mock/api/access.ts b/src/mock/api/access.ts new file mode 100644 index 0000000..ccdbfc6 --- /dev/null +++ b/src/mock/api/access.ts @@ -0,0 +1,53 @@ +interface AccessMock { + build: (api: API) => { + deny: (reason: string) => API; + }; + state: { + denied: false | { reason: string }; + }; +}; + +interface CredentialsExchangeAccessMock { + build: (api: API) => { + deny: (code: string, reason: string) => API; + }; + state: { + denied: false | { code: string; reason: string }; + }; +}; + +export function accessMock(flow: "CredentialsExchange"): CredentialsExchangeAccessMock; +export function accessMock(flow: string): AccessMock; +export function accessMock(flow: string) { + switch(flow) { + case "CredentialsExchange": { + const state = { + denied: false as false | { code: string; reason: string }, + }; + + const build = (api: API) => ({ + deny: (code: string, reason: string) => { + state.denied = { code, reason }; + return api; + }, + }); + + return { build, state }; + } + + default: { + const state = { + denied: false as false | { reason: string }, + }; + + const build = (api: API) => ({ + deny: (reason: string) => { + state.denied = { reason }; + return api; + }, + }); + + return { build, state }; + } + } +} diff --git a/src/mock/api/credentials-exchange.ts b/src/mock/api/credentials-exchange.ts index 8852c00..88921f7 100644 --- a/src/mock/api/credentials-exchange.ts +++ b/src/mock/api/credentials-exchange.ts @@ -2,13 +2,14 @@ import Auth0 from "../../types"; import { cache as mockCache } from "./cache"; import { request as mockRequest } from "../request"; import { accessTokenMock } from "./access-token"; +import { accessMock } from "./access"; export interface CredentialsExchangeOptions { cache?: Record; } export interface CredentialsExchangeState { - access: { denied: false } | { denied: true; code: string; reason: string }; + access: { denied: false } | { denied: { code: string; reason: string } }; accessToken: { claims: Record; }; @@ -19,20 +20,18 @@ export function credentialsExchange({ cache, }: CredentialsExchangeOptions = {}) { const apiCache = mockCache(cache); + const access = accessMock("CredentialsExchange"); const accessToken = accessTokenMock("CredentialsExchange") const state: CredentialsExchangeState = { - access: { denied: false }, + access: access.state, accessToken: accessToken.state, cache: apiCache, }; const api: Auth0.API.CredentialsExchange = { - access: { - deny: (code, reason) => { - state.access = { denied: true, code, reason }; - return api; - }, + get access() { + return access.build(api); }, get accessToken() { diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index 8ce3632..8f911e1 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -5,6 +5,7 @@ import { request as mockRequest } from "../request"; import { ok } from "node:assert"; import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; import { accessTokenMock } from "./access-token"; +import { accessMock } from "./access"; export interface PostLoginOptions { user?: Auth0.User; @@ -53,7 +54,7 @@ export interface PostLoginState { user: Auth0.User; primaryUserId: string; cache: Auth0.API.Cache; - access: { denied: false } | { denied: true; reason: string }; + access: { denied: false } | { denied: { reason: string } }; accessToken: { claims: Record; scopes: string[]; @@ -89,6 +90,7 @@ export function postLogin({ now: nowValue, }: PostLoginOptions = {}) { const apiCache = mockCache(cache); + const access = accessMock("PostLogin"); const accessToken = accessTokenMock("PostLogin"); const executedRules = optionallyExecutedRules ?? []; const userValue = user ?? mockUser(); @@ -101,7 +103,7 @@ export function postLogin({ const state: PostLoginState = { user: userValue, primaryUserId: userValue.user_id, - access: { denied: false }, + access: access.state, accessToken: accessToken.state, authentication: { challenge: false, @@ -177,11 +179,8 @@ export function postLogin({ } const api: Auth0.API.PostLogin = { - access: { - deny: (reason) => { - state.access = { denied: true, reason }; - return api; - }, + get access() { + return access.build(api) }, get accessToken() { diff --git a/src/test/api/access.test.ts b/src/test/api/access.test.ts new file mode 100644 index 0000000..ff03dee --- /dev/null +++ b/src/test/api/access.test.ts @@ -0,0 +1,44 @@ +import test from "node:test"; +import { accessMock } from "../../mock/api/access"; +import { ok, strictEqual } from "node:assert"; + +test("access mock", async (t) => { + const baseApi = Symbol("Base API"); + + await t.test("allowed", async (t) => { + const { build, state } = accessMock("Another Flow"); + const api = build(baseApi); + + ok(!state.denied, "Expected access to be allowed"); + }); + + await t.test("denied", async (t) => { + const { build, state } = accessMock("Another Flow"); + const api = build(baseApi); + + strictEqual(api.deny("Must be an admin"), baseApi, "Expected base api to be returned"); + + ok(state.denied, "Expected access to be denied"); + strictEqual(state.denied.reason, "Must be an admin", "Expected reason to be set"); + }); + + await t.test("CredentialsExchange", async (t) => { + await t.test("allowed", async (t) => { + const { build, state } = accessMock("CredentialsExchange"); + const api = build(baseApi); + + ok(!state.denied, "Expected access to be allowed"); + }); + + await t.test("denied", async (t) => { + const { build, state } = accessMock("CredentialsExchange"); + const api = build(baseApi); + + strictEqual(api.deny("invalid_scope", "Must be an admin"), baseApi, "Expected base api to be returned"); + + ok(state.denied, "Expected access to be denied"); + strictEqual(state.denied.code, "invalid_scope", "Expected code to be set"); + strictEqual(state.denied.reason, "Must be an admin", "Expected reason to be set"); + }); + }); +}); diff --git a/src/test/api/post-login.test.ts b/src/test/api/post-login.test.ts index 1e951ad..a4e78ad 100644 --- a/src/test/api/post-login.test.ts +++ b/src/test/api/post-login.test.ts @@ -10,10 +10,8 @@ test("PostLogin API", async (t) => { strictEqual(api.access.deny("Only cool kids allowed"), api); - deepStrictEqual(state.access, { - denied: true, - reason: "Only cool kids allowed", - }); + ok(state.access.denied, "Expected access to be denied"); + strictEqual(state.access.denied.reason, "Only cool kids allowed"); }); await t.test("accessToken", async (t) => { From c9410d76b6dc10ed8a515ec8ca9605fdc6ff16d9 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Wed, 10 Apr 2024 16:32:49 +1200 Subject: [PATCH 03/13] Extract mock authentication api --- src/mock/api/authentication.ts | 65 ++++++++++++++++ src/mock/api/post-login.ts | 71 +++--------------- src/test/api/authentication.test.ts | 111 ++++++++++++++++++++++++++++ src/test/api/post-login.test.ts | 4 +- 4 files changed, 188 insertions(+), 63 deletions(-) create mode 100644 src/mock/api/authentication.ts create mode 100644 src/test/api/authentication.test.ts diff --git a/src/mock/api/authentication.ts b/src/mock/api/authentication.ts new file mode 100644 index 0000000..6ccf531 --- /dev/null +++ b/src/mock/api/authentication.ts @@ -0,0 +1,65 @@ +import { Factor } from "../../types"; + +export interface FactorList { + allOptions: Factor[]; + default: Factor | undefined; +} + +export function authenticationMock(_flow: string, { userId }: { userId: string }) { + let numCallsToSetPrimaryUser = 0; + + const state = { + primaryUserId: userId, + challenge: false as false | FactorList, + enrollment: false as false | FactorList, + newlyRecordedMethods: [] as string[], + }; + + const build = (api: T) => ({ + challengeWith: (factor: Factor, options?: { additionalFactors?: Factor[] }) => { + const additionalFactors = options?.additionalFactors ?? []; + + state.challenge = { + allOptions: [factor, ...additionalFactors], + default: factor, + }; + }, + challengeWithAny(factors: Factor[]) { + state.challenge = { + allOptions: factors, + default: undefined, + }; + }, + enrollWith: (factor: Factor, options?: { additionalFactors?: Factor[] }) => { + const additionalFactors = options?.additionalFactors ?? []; + + state.enrollment = { + allOptions: [factor, ...additionalFactors], + default: factor, + }; + }, + enrollWithAny(factors: Factor[]) { + state.enrollment = { + allOptions: factors, + default: undefined, + }; + }, + setPrimaryUser: (primaryUserId: string) => { + numCallsToSetPrimaryUser++; + + if (numCallsToSetPrimaryUser > 1) { + throw new Error( + "`authentication.setPrimaryUser` can only be set once per transaction" + ); + } + + state.primaryUserId = primaryUserId; + }, + recordMethod: (providerUrl: string) => { + state.newlyRecordedMethods.push(providerUrl); + return api; + }, + }); + + return { state, build }; +} diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index 8f911e1..f4191f4 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -6,6 +6,7 @@ import { ok } from "node:assert"; import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; import { accessTokenMock } from "./access-token"; import { accessMock } from "./access"; +import { authenticationMock, FactorList } from "./authentication"; export interface PostLoginOptions { user?: Auth0.User; @@ -45,14 +46,8 @@ interface SamlResponseState { signingCert?: string; } -interface FactorList { - allOptions: Factor[]; - default: Factor | undefined; -} - export interface PostLoginState { user: Auth0.User; - primaryUserId: string; cache: Auth0.API.Cache; access: { denied: false } | { denied: { reason: string } }; accessToken: { @@ -60,6 +55,7 @@ export interface PostLoginState { scopes: string[]; }; authentication: { + primaryUserId: string; challenge: FactorList | false; enrollment: FactorList | false; newlyRecordedMethods: string[]; @@ -89,27 +85,22 @@ export function postLogin({ executedRules: optionallyExecutedRules, now: nowValue, }: PostLoginOptions = {}) { + const userValue = user ?? mockUser(); + const executedRules = optionallyExecutedRules ?? []; + const requestValue = request ?? mockRequest(); + const apiCache = mockCache(cache); const access = accessMock("PostLogin"); const accessToken = accessTokenMock("PostLogin"); - const executedRules = optionallyExecutedRules ?? []; - const userValue = user ?? mockUser(); - const requestValue = request ?? mockRequest(); + const authentication = authenticationMock("PostLogin", { userId: userValue.user_id }); const now = new Date(nowValue || Date.now()); - let numCallsToSetPrimaryUser = 0; - const state: PostLoginState = { user: userValue, - primaryUserId: userValue.user_id, access: access.state, accessToken: accessToken.state, - authentication: { - challenge: false, - enrollment: false, - newlyRecordedMethods: [], - }, + authentication: authentication.state, cache: apiCache, idToken: { claims: {}, @@ -187,50 +178,8 @@ export function postLogin({ return accessToken.build(api); }, - authentication: { - challengeWith: (factor, options) => { - const additionalFactors = options?.additionalFactors ?? []; - - state.authentication.challenge = { - allOptions: [factor, ...additionalFactors], - default: factor, - }; - }, - challengeWithAny(factors) { - state.authentication.challenge = { - allOptions: factors, - default: undefined, - }; - }, - enrollWith(factor, options) { - const additionalFactors = options?.additionalFactors ?? []; - - state.authentication.enrollment = { - allOptions: [factor, ...additionalFactors], - default: factor, - }; - }, - enrollWithAny(factors) { - state.authentication.enrollment = { - allOptions: factors, - default: undefined, - }; - }, - setPrimaryUser: (primaryUserId) => { - numCallsToSetPrimaryUser++; - - if (numCallsToSetPrimaryUser > 1) { - throw new Error( - "`authentication.setPrimaryUser` can only be set once per transaction" - ); - } - - state.primaryUserId = primaryUserId; - }, - recordMethod: (providerUrl) => { - state.authentication.newlyRecordedMethods.push(providerUrl); - return api; - }, + get authentication() { + return authentication.build(api); }, cache: apiCache, diff --git a/src/test/api/authentication.test.ts b/src/test/api/authentication.test.ts new file mode 100644 index 0000000..7955e55 --- /dev/null +++ b/src/test/api/authentication.test.ts @@ -0,0 +1,111 @@ +import test from "node:test"; +import { authenticationMock } from "../../mock/api/authentication"; +import { ok, strictEqual } from "node:assert"; + +test("authentication mock", async (t) => { + const baseApi = Symbol("Base API"); + + await t.test("challengeWith", async (t) => { + await t.test("factor", async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.challengeWith({ type: "email" }); + + ok(state.challenge, "Expected challenge to be set"); + strictEqual(state.challenge.default?.type, "email", "Expected default factor to be set"); + strictEqual(state.challenge.allOptions?.length, 1, "Expected additional factors to be set"); + strictEqual(state.challenge.allOptions?.[0].type, "email", "Expected additional factors to be email"); + }); + + await t.test("additional factors", async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.challengeWith({ type: "otp" }, { additionalFactors: [{ type: "email" }] }); + + ok(state.challenge, "Expected challenge to be set"); + strictEqual(state.challenge.default?.type, "otp", "Expected default factor to be set"); + strictEqual(state.challenge.allOptions?.length, 2, "Expected additional factors to be set"); + strictEqual(state.challenge.allOptions?.[0].type, "otp", "Expected additional factor to be otp"); + strictEqual(state.challenge.allOptions?.[1].type, "email", "Expected additional factor to be email"); + }); + }); + + await t.test('challengeWithAny', async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.challengeWithAny([{ type: "email" }, { type: "otp" }]); + + ok(state.challenge, "Expected challenge to be set"); + strictEqual(state.challenge.default, undefined, "Expected default factor to be undefined"); + strictEqual(state.challenge.allOptions?.length, 2, "Expected additional factors to be set"); + strictEqual(state.challenge.allOptions?.[0].type, "email", "Expected additional factor to be email"); + strictEqual(state.challenge.allOptions?.[1].type, "otp", "Expected additional factor to be otp"); + }); + + await t.test("enrollWith", async (t) => { + await t.test("factor", async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.enrollWith({ type: "email" }); + + ok(state.enrollment, "Expected enrollment to be set"); + strictEqual(state.enrollment.default?.type, "email", "Expected default factor to be set"); + strictEqual(state.enrollment.allOptions?.length, 1, "Expected additional factors to be set"); + strictEqual(state.enrollment.allOptions?.[0].type, "email", "Expected additional factors to be email"); + }); + + await t.test("additional factors", async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.enrollWith({ type: "otp" }, { additionalFactors: [{ type: "email" }] }); + + ok(state.enrollment, "Expected enrollment to be set"); + strictEqual(state.enrollment.default?.type, "otp", "Expected default factor to be set"); + strictEqual(state.enrollment.allOptions?.length, 2, "Expected additional factors to be set"); + strictEqual(state.enrollment.allOptions?.[0].type, "otp", "Expected additional factor to be otp"); + strictEqual(state.enrollment.allOptions?.[1].type, "email", "Expected additional factor to be email"); + }); + }); + + await t.test('enrollWithAny', async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.enrollWithAny([{ type: "email" }, { type: "otp" }]); + + ok(state.enrollment, "Expected enrollment to be set"); + strictEqual(state.enrollment.default, undefined, "Expected default factor to be undefined"); + strictEqual(state.enrollment.allOptions?.length, 2, "Expected additional factors to be set"); + strictEqual(state.enrollment.allOptions?.[0].type, "email", "Expected additional factor to be email"); + strictEqual(state.enrollment.allOptions?.[1].type, "otp", "Expected additional factor to be otp"); + }); + + await t.test("setPrimaryUser", async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.setPrimaryUser("43"); + + strictEqual(state.primaryUserId, "43", "Expected primary user to be set"); + }); + + await t.test("recordMethod", async (t) => { + const { build, state } = authenticationMock("Another Flow", { userId: "42" }); + const api = build(baseApi); + + api.recordMethod("https://example.com"); + + strictEqual(state.newlyRecordedMethods.length, 1, "Expected newly recorded methods to be set"); + strictEqual(state.newlyRecordedMethods[0], "https://example.com", "Expected newly recorded method to be set"); + + api.recordMethod("https://another.example.com"); + + strictEqual(state.newlyRecordedMethods.length, 2, "Expected newly recorded methods to be set"); + strictEqual(state.newlyRecordedMethods[1], "https://another.example.com", "Expected newly recorded method to be set"); + }); +}); diff --git a/src/test/api/post-login.test.ts b/src/test/api/post-login.test.ts index a4e78ad..f163d81 100644 --- a/src/test/api/post-login.test.ts +++ b/src/test/api/post-login.test.ts @@ -379,14 +379,14 @@ test("PostLogin API", async (t) => { await t.test("can change the primary user ID", async (t) => { const { implementation: api, state } = postLogin(); const originalUserId = state.user.user_id; - strictEqual(state.primaryUserId, originalUserId); + strictEqual(state.authentication.primaryUserId, originalUserId); strictEqual( api.authentication.setPrimaryUser("new-primary-user-id"), undefined ); - strictEqual(state.primaryUserId, "new-primary-user-id"); + strictEqual(state.authentication.primaryUserId, "new-primary-user-id"); strictEqual(state.user.user_id, originalUserId); }); From d8e232d22cf2bbaa92535bf6e0584293ee5e5c9c Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Wed, 10 Apr 2024 16:49:35 +1200 Subject: [PATCH 04/13] Extract mock id-token api --- src/mock/api/id-token.ts | 14 ++++++++++++++ src/mock/api/post-login.ts | 13 +++++-------- src/test/api/id-token.test.ts | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 src/mock/api/id-token.ts create mode 100644 src/test/api/id-token.test.ts diff --git a/src/mock/api/id-token.ts b/src/mock/api/id-token.ts new file mode 100644 index 0000000..8613e1a --- /dev/null +++ b/src/mock/api/id-token.ts @@ -0,0 +1,14 @@ +export function idTokenMock(flow: string) { + const state = { + claims: {} as Record, + }; + + const build = (api: T) => ({ + setCustomClaim: (name: string, value: unknown) => { + state.claims[name] = value; + return api; + }, + }); + + return { state, build }; +} diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index f4191f4..65bdfd5 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -7,6 +7,7 @@ import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; import { accessTokenMock } from "./access-token"; import { accessMock } from "./access"; import { authenticationMock, FactorList } from "./authentication"; +import { idTokenMock } from "./id-token"; export interface PostLoginOptions { user?: Auth0.User; @@ -93,6 +94,7 @@ export function postLogin({ const access = accessMock("PostLogin"); const accessToken = accessTokenMock("PostLogin"); const authentication = authenticationMock("PostLogin", { userId: userValue.user_id }); + const idToken = idTokenMock("PostLogin"); const now = new Date(nowValue || Date.now()); @@ -102,9 +104,7 @@ export function postLogin({ accessToken: accessToken.state, authentication: authentication.state, cache: apiCache, - idToken: { - claims: {}, - }, + idToken: idToken.state, multifactor: { enabled: false, }, @@ -184,11 +184,8 @@ export function postLogin({ cache: apiCache, - idToken: { - setCustomClaim: (name, value) => { - state.idToken.claims[name] = value; - return api; - }, + get idToken() { + return idToken.build(api); }, multifactor: { diff --git a/src/test/api/id-token.test.ts b/src/test/api/id-token.test.ts new file mode 100644 index 0000000..9c9ab41 --- /dev/null +++ b/src/test/api/id-token.test.ts @@ -0,0 +1,15 @@ +import test from "node:test"; +import { idTokenMock } from "../../mock/api/id-token"; +import { deepStrictEqual, strictEqual } from "node:assert"; + +test('idToken', async (t) => { + const baseApi = Symbol("baseApi") + await t.test('setCustomClaim', () => { + const { build, state } = idTokenMock("Another Flow"); + const api = build(baseApi); + + strictEqual(api.setCustomClaim("name", "Alice"), baseApi, "Expected base api to be returned"); + + deepStrictEqual(state.claims.name, "Alice", "Expected custom claim to be set"); + }) +}); From 80ec89110d11a22b5f86430c276f7173d33fd034 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Wed, 10 Apr 2024 16:57:00 +1200 Subject: [PATCH 05/13] Extract mock multifactor api --- src/mock/api/multifactor.ts | 16 ++++++++++++++++ src/mock/api/post-login.ts | 13 +++++-------- src/test/api/multifactor.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/mock/api/multifactor.ts create mode 100644 src/test/api/multifactor.test.ts diff --git a/src/mock/api/multifactor.ts b/src/mock/api/multifactor.ts new file mode 100644 index 0000000..d45b06f --- /dev/null +++ b/src/mock/api/multifactor.ts @@ -0,0 +1,16 @@ +import { MultifactorEnableOptions } from "../../types"; + +export function multifactorMock(flow: string) { + const state = { + enabled: false as false | { provider: string; options?: MultifactorEnableOptions }, + }; + + const build = (api: T) => ({ + enable: (provider: string, options?: MultifactorEnableOptions) => { + state.enabled = { provider, options }; + return api; + }, + }); + + return { state, build }; +} diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index 65bdfd5..cdb5d1f 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -8,6 +8,7 @@ import { accessTokenMock } from "./access-token"; import { accessMock } from "./access"; import { authenticationMock, FactorList } from "./authentication"; import { idTokenMock } from "./id-token"; +import { multifactorMock } from "./multifactor"; export interface PostLoginOptions { user?: Auth0.User; @@ -95,6 +96,7 @@ export function postLogin({ const accessToken = accessTokenMock("PostLogin"); const authentication = authenticationMock("PostLogin", { userId: userValue.user_id }); const idToken = idTokenMock("PostLogin"); + const multifactor = multifactorMock("PostLogin"); const now = new Date(nowValue || Date.now()); @@ -105,9 +107,7 @@ export function postLogin({ authentication: authentication.state, cache: apiCache, idToken: idToken.state, - multifactor: { - enabled: false, - }, + multifactor: multifactor.state, samlResponse: { // Custom attributes attributes: {}, @@ -188,11 +188,8 @@ export function postLogin({ return idToken.build(api); }, - multifactor: { - enable: (provider, options) => { - state.multifactor.enabled = { provider, options }; - return api; - }, + get multifactor() { + return multifactor.build(api); }, redirect: { diff --git a/src/test/api/multifactor.test.ts b/src/test/api/multifactor.test.ts new file mode 100644 index 0000000..10980e3 --- /dev/null +++ b/src/test/api/multifactor.test.ts @@ -0,0 +1,29 @@ +import { deepStrictEqual, ok, strictEqual } from "node:assert"; +import test from "node:test"; +import { multifactorMock } from "../../mock/api/multifactor"; + +test("multifactor", async (t) => { + const baseApi = Symbol("Base API"); + + await t.test("enable", async (t) => { + const { build, state } = multifactorMock("Another Flow"); + const api = build(baseApi); + + strictEqual(state.enabled, false); + + const options = { + allowRememberBrowser: true, + providerOptions: { + host: "custom-host", + ikey: "custom-ikey", + skey: "custom-skey", + username: "custom-username", + }, + }; + + strictEqual(api.enable("duo", options), baseApi, "Expected base api to be returned"); + + ok(state.enabled, "Expected multifactor to be enabled"); + deepStrictEqual(state.enabled, { provider: "duo", options }); + }); +}); From 7febfa08ac4dbac517d70395fef474c1dde2ed7f Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Wed, 10 Apr 2024 17:36:13 +1200 Subject: [PATCH 06/13] Extract mock redirect api --- examples/redirect.test.js | 8 +- src/mock/api/post-login.ts | 101 ++---------- src/mock/api/redirect.ts | 126 +++++++++++++++ src/test/api/post-login.test.ts | 12 +- src/test/api/redirect.test.ts | 268 ++++++++++++++++++++++++++++++++ 5 files changed, 413 insertions(+), 102 deletions(-) create mode 100644 src/mock/api/redirect.ts create mode 100644 src/test/api/redirect.test.ts diff --git a/examples/redirect.test.js b/examples/redirect.test.js index 64c15d7..faa5288 100644 --- a/examples/redirect.test.js +++ b/examples/redirect.test.js @@ -21,26 +21,26 @@ test("redirect and continue with signed data", async (t) => { const { redirect } = action; strict( - redirect.queryParams.theme, + redirect.target.queryParams.theme, "spiffy", "Unexpected value for `theme` query parameter" ); strictEqual( // You can also use redirect.url.href to get the full URL as a string - redirect.url.origin, + redirect.target.url.origin, "https://example.com", "Unexpected redirect URL origin" ); strictEqual( - redirect.url.pathname, + redirect.target.url.pathname, "/sandwich-preferences", "Unexpected redirect URL path" ); // Test the signed JWT data payload - const { session_token } = redirect.queryParams; + const { session_token } = redirect.target.queryParams; const decoded = jwt.decodeJWTPayload(session_token); diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index cdb5d1f..f0da110 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -9,6 +9,7 @@ import { accessMock } from "./access"; import { authenticationMock, FactorList } from "./authentication"; import { idTokenMock } from "./id-token"; import { multifactorMock } from "./multifactor"; +import { redirectMock } from "./redirect"; export interface PostLoginOptions { user?: Auth0.User; @@ -77,7 +78,9 @@ export interface PostLoginState { validation: { error: { code: string; message: string } | null; }; - redirect: { url: URL; queryParams: Record } | null; + redirect: { + target: { url: URL; queryParams: Record } | null; + }; } export function postLogin({ @@ -90,6 +93,7 @@ export function postLogin({ const userValue = user ?? mockUser(); const executedRules = optionallyExecutedRules ?? []; const requestValue = request ?? mockRequest(); + const now = new Date(nowValue || Date.now()); const apiCache = mockCache(cache); const access = accessMock("PostLogin"); @@ -97,8 +101,7 @@ export function postLogin({ const authentication = authenticationMock("PostLogin", { userId: userValue.user_id }); const idToken = idTokenMock("PostLogin"); const multifactor = multifactorMock("PostLogin"); - - const now = new Date(nowValue || Date.now()); + const redirect = redirectMock("PostLogin", { now, request: requestValue, user: userValue }); const state: PostLoginState = { user: userValue, @@ -142,7 +145,7 @@ export function postLogin({ validation: { error: null, }, - redirect: null, + redirect: redirect.state, }; const samlResponse = { @@ -192,94 +195,8 @@ export function postLogin({ return multifactor.build(api); }, - redirect: { - encodeToken: ({ expiresInSeconds, payload, secret }) => { - expiresInSeconds = expiresInSeconds ?? 900; - - const claims = { - iss: requestValue.hostname, - iat: Math.floor(now.getTime() / 1000), - exp: Math.floor((now.getTime() + expiresInSeconds * 1000) / 1000), - sub: userValue.user_id, - ip: requestValue.ip, - ...payload, - }; - - return encodeHS256JWT({ secret, claims }); - }, - - sendUserTo: (urlString, options) => { - 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.redirect = { url, queryParams }; - - return api; - }, - - validateToken: ({ tokenParameterName, secret }) => { - tokenParameterName = tokenParameterName ?? "session_token"; - const params = { ...requestValue.query, ...requestValue.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 === userValue.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; - }, + get redirect() { + return redirect.build(api); }, rules: { diff --git a/src/mock/api/redirect.ts b/src/mock/api/redirect.ts new file mode 100644 index 0000000..36337c1 --- /dev/null +++ b/src/mock/api/redirect.ts @@ -0,0 +1,126 @@ +import { ok } from "assert"; +import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; +import { Request, User } from "../../types"; + +interface RedirectMockOptions { + readonly now?: ConstructorParameters[0]; + readonly request: Request; + readonly user: User; +} + +interface EncodeTokenOptions { + expiresInSeconds?: number | undefined; + payload: { + [key: string]: unknown; + }; + secret: string; +} + +interface SendUserToOptions { + query?: Record; +} + +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 }, + }; + + const build = (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; + }, + }); + + return { state, build }; +} diff --git a/src/test/api/post-login.test.ts b/src/test/api/post-login.test.ts index f163d81..39993de 100644 --- a/src/test/api/post-login.test.ts +++ b/src/test/api/post-login.test.ts @@ -424,9 +424,9 @@ test("PostLogin API", async (t) => { const { redirect } = state; - ok(redirect, "redirect not set"); - deepStrictEqual(redirect.queryParams, {}, "query should be empty"); - strictEqual(redirect.url.href, "https://example.com/r", "url mismatch"); + ok(redirect.target, "redirect not set"); + deepStrictEqual(redirect.target.queryParams, {}, "query should be empty"); + strictEqual(redirect.target.url.href, "https://example.com/r", "url mismatch"); }); await t.test("redirect with consolidated GET parameters", async (t) => { @@ -441,16 +441,16 @@ test("PostLogin API", async (t) => { const { redirect } = state; - ok(redirect, "redirect not set"); + ok(redirect.target, "redirect not set"); deepStrictEqual( - redirect.queryParams, + redirect.target.queryParams, { bread: "rye", filling: "cheese", spread: "butter" }, "unexpected query" ); strictEqual( - redirect.url.href, + redirect.target.url.href, "https://example.com/?bread=rye&filling=cheese&spread=butter", "url mismatch" ); diff --git a/src/test/api/redirect.test.ts b/src/test/api/redirect.test.ts new file mode 100644 index 0000000..088c659 --- /dev/null +++ b/src/test/api/redirect.test.ts @@ -0,0 +1,268 @@ +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 + ); + }); + } + }); +}); From 67c2fbb701c53c48af547b0a2f7c1a7ac706f91c Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 09:32:44 +1200 Subject: [PATCH 07/13] Extract mock user api --- src/mock/api/post-login.ts | 17 +++++------------ src/mock/api/user.ts | 20 ++++++++++++++++++++ src/test/api/user.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 src/mock/api/user.ts create mode 100644 src/test/api/user.test.ts diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index f0da110..2897a36 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -10,6 +10,7 @@ import { authenticationMock, FactorList } from "./authentication"; import { idTokenMock } from "./id-token"; import { multifactorMock } from "./multifactor"; import { redirectMock } from "./redirect"; +import { userMock } from "./user"; export interface PostLoginOptions { user?: Auth0.User; @@ -102,9 +103,10 @@ export function postLogin({ const idToken = idTokenMock("PostLogin"); const multifactor = multifactorMock("PostLogin"); const redirect = redirectMock("PostLogin", { now, request: requestValue, user: userValue }); + const userApiMock = userMock("PostLogin", { user: userValue }); const state: PostLoginState = { - user: userValue, + user: userApiMock.state, access: access.state, accessToken: accessToken.state, authentication: authentication.state, @@ -207,17 +209,8 @@ export function postLogin({ samlResponse, - user: { - setAppMetadata: (key, value) => { - state.user.app_metadata ??= {}; - state.user.app_metadata[key] = value; - return api; - }, - setUserMetadata: (key, value) => { - state.user.user_metadata ??= {}; - state.user.user_metadata[key] = value; - return api; - }, + get user() { + return userApiMock.build(api); }, validation: { diff --git a/src/mock/api/user.ts b/src/mock/api/user.ts new file mode 100644 index 0000000..8637889 --- /dev/null +++ b/src/mock/api/user.ts @@ -0,0 +1,20 @@ +import { User } from "../../types"; + +export function userMock(flow: string, { user }: { user: User}) { + const state = user + + const build = (api: T) => ({ + setAppMetadata: (key: string, value: unknown) => { + state.app_metadata ??= {}; + state.app_metadata[key] = value; + return api; + }, + setUserMetadata: (key: string, value: unknown) => { + state.user_metadata ??= {}; + state.user_metadata[key] = value; + return api; + }, + }); + + return { build, state } +} diff --git a/src/test/api/user.test.ts b/src/test/api/user.test.ts new file mode 100644 index 0000000..0e29e96 --- /dev/null +++ b/src/test/api/user.test.ts @@ -0,0 +1,26 @@ +import test from "node:test"; +import { userMock } from "../../mock/api/user"; +import { deepStrictEqual, strictEqual } from "node:assert"; +import { user } from "../../mock"; + +test("user", async (t) => { + const baseApi = Symbol("Base API"); + + await t.test('setAppMetadata', (t) => { + const { build, state } = userMock("Another Flow", { user: user() }); + const api = build(baseApi); + + strictEqual(api.setAppMetadata("name", "Alice"), baseApi, "Expected base api to be returned"); + + deepStrictEqual(state.app_metadata, { name: "Alice" }, "Expected app metadata to be set"); + }); + + await t.test('setUserMetadata', (t) => { + const { build, state } = userMock("Another Flow", { user: user() }); + const api = build(baseApi); + + strictEqual(api.setUserMetadata("name", "Alice"), baseApi, "Expected base api to be returned"); + + deepStrictEqual(state.user_metadata, { name: "Alice" }, "Expected user metadata to be set"); + }); +}); From beca9d913623eb8ca1967111245246083ab1b57d Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 10:37:28 +1200 Subject: [PATCH 08/13] Extract mock SAML response api --- src/mock/api/post-login.ts | 62 ++----------- src/mock/api/saml-response.ts | 123 +++++++++++++++++++++++++ src/test/api/saml-response.test.ts | 138 +++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 56 deletions(-) create mode 100644 src/mock/api/saml-response.ts create mode 100644 src/test/api/saml-response.test.ts diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index 2897a36..f7e942a 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -11,6 +11,7 @@ import { idTokenMock } from "./id-token"; import { multifactorMock } from "./multifactor"; import { redirectMock } from "./redirect"; import { userMock } from "./user"; +import { samlResponseMock } from "./saml-response"; export interface PostLoginOptions { user?: Auth0.User; @@ -104,6 +105,7 @@ export function postLogin({ const multifactor = multifactorMock("PostLogin"); const redirect = redirectMock("PostLogin", { now, request: requestValue, user: userValue }); const userApiMock = userMock("PostLogin", { user: userValue }); + const samlResponse = samlResponseMock("PostLogin"); const state: PostLoginState = { user: userApiMock.state, @@ -113,67 +115,13 @@ export function postLogin({ cache: apiCache, idToken: idToken.state, multifactor: multifactor.state, - samlResponse: { - // Custom attributes - attributes: {}, - - // Default literal values - createUpnClaim: true, - passthroughClaimsWithNoMapping: true, - mapUnknownClaimsAsIs: false, - mapIdentities: true, - signResponse: false, - includeAttributeNameFormat: true, - typedAttributes: true, - lifetimeInSeconds: 3600, - - nameIdentifierFormat: - "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", - - nameIdentifierProbes: [ - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", - "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", - ], - - authnContextClassRef: - "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified", - - // Default dynamic values - audience: "default-audience", - recipient: "default-recipient", - destination: "default-destination", - }, + samlResponse: samlResponse.state, validation: { error: null, }, redirect: redirect.state, }; - const samlResponse = { - setAttribute: (attribute: string, value: SamlAttributeValue) => { - state.samlResponse.attributes[attribute] = value; - }, - } as Auth0.API.PostLogin["samlResponse"]; - - for (const property in state.samlResponse) { - if (state.samlResponse.hasOwnProperty(property)) { - const key = property as keyof SamlResponseState; - - if (key === "attributes") { - continue; - } - - const setter = `set${key[0].toUpperCase()}${key.slice( - 1 - )}` as keyof Auth0.API.PostLogin["samlResponse"]; - - samlResponse[setter] = (value: unknown) => { - state.samlResponse[key] = value as never; - }; - } - } - const api: Auth0.API.PostLogin = { get access() { return access.build(api) @@ -207,7 +155,9 @@ export function postLogin({ }, }, - samlResponse, + get samlResponse() { + return samlResponse.build(api); + }, get user() { return userApiMock.build(api); diff --git a/src/mock/api/saml-response.ts b/src/mock/api/saml-response.ts new file mode 100644 index 0000000..5de56ff --- /dev/null +++ b/src/mock/api/saml-response.ts @@ -0,0 +1,123 @@ +type SamlAttributeValue = + | string + | number + | boolean + | null + | Array; + +interface SamlResponseState { + attributes: Record; + createUpnClaim: boolean; + passthroughClaimsWithNoMapping: boolean; + mapUnknownClaimsAsIs: boolean; + mapIdentities: boolean; + signResponse: boolean; + lifetimeInSeconds: number; + nameIdentifierFormat: string; + nameIdentifierProbes: string[]; + authnContextClassRef: string; + includeAttributeNameFormat: boolean; + typedAttributes: boolean; + audience: string; + recipient: string; + destination: string; + cert?: string; + encryptionCert?: string; + encryptionPublicKey?: string; + key?: string; + signingCert?: string; +} + +interface SamlResponse { + setAttribute( + attribute: string, + value: string | number | boolean | null | (string | number | boolean)[] + ): void; + setAudience(audience: string): void; + setRecipient(recipient: string): void; + setCreateUpnClaim(createUpnClaim: boolean): void; + setPassthroughClaimsWithNoMapping( + passthroughClaimsWithNoMapping: boolean + ): void; + setMapUnknownClaimsAsIs(mapUnknownClaimsAsIs: boolean): void; + setMapIdentities(mapIdentities: boolean): void; + setSignatureAlgorithm(signatureAlgorithm: "rsa-sha256"): void; + setSignatureAlgorithm(signatureAlgorithm: "rsa-sha1"): void; + setDigestAlgorithm(digestAlgorithm: "sha256"): void; + setDigestAlgorithm(digestAlgorithm: "sha1"): void; + setDestination(destination: string): void; + setLifetimeInSeconds(lifetimeInSeconds: number): void; + setSignResponse(signResponse: boolean): void; + setNameIdentifierFormat(nameIdentifierFormat: string): void; + setNameIdentifierProbes(nameIdentifierProbes: string[]): void; + setAuthnContextClassRef(authnContextClassRef: string): void; + setSigningCert(signingCert: string): void; + setIncludeAttributeNameFormat(includeAttributeNameFormat: boolean): void; + setTypedAttributes(typedAttributes: boolean): void; + setEncryptionCert(encryptionCert: string): void; + setEncryptionPublicKey(encryptionPublicKey: string): void; + setCert(cert: string): void; + setKey(key: string): void; +} + +export function samlResponseMock(flow: string) { + const state: SamlResponseState = { + // Custom attributes + attributes: {}, + + // Default literal values + createUpnClaim: true, + passthroughClaimsWithNoMapping: true, + mapUnknownClaimsAsIs: false, + mapIdentities: true, + signResponse: false, + includeAttributeNameFormat: true, + typedAttributes: true, + lifetimeInSeconds: 3600, + + nameIdentifierFormat: + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + + nameIdentifierProbes: [ + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", + ], + + authnContextClassRef: + "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified", + + // Default dynamic values + audience: "default-audience", + recipient: "default-recipient", + destination: "default-destination", + }; + + const samlResponse = { + setAttribute: (attribute: string, value: SamlAttributeValue) => { + state.attributes[attribute] = value; + }, + } as SamlResponse; + + for (const property in state) { + if (state.hasOwnProperty(property)) { + const key = property as keyof SamlResponseState; + + if (key === "attributes") { + continue; + } + + const setter = `set${key[0].toUpperCase()}${key.slice( + 1 + )}` as keyof SamlResponse; + + samlResponse[setter] = (value: unknown) => { + state[key] = value as never; + }; + } + } + + const build = (api: T) => samlResponse; + + return { state, build }; +} diff --git a/src/test/api/saml-response.test.ts b/src/test/api/saml-response.test.ts new file mode 100644 index 0000000..74b5723 --- /dev/null +++ b/src/test/api/saml-response.test.ts @@ -0,0 +1,138 @@ +import test from "node:test"; +import { strictEqual, deepStrictEqual, throws, ok } from "node:assert"; +import { samlResponseMock } from "../../mock/api/saml-response"; + +test("Saml Response", async (t) => { + const baseApi = Symbol("Base API"); + const { build, state } = samlResponseMock("Another Flow"); + const api = build(baseApi); + + await t.test("attributes", async (t) => { + deepStrictEqual(state.attributes, {}); + api.setAttribute("species", "cat"); + api.setAttribute("nickname", "Buddy"); + deepStrictEqual(state.attributes, { + species: "cat", + nickname: "Buddy", + }); + }); + + await t.test("audience", async (t) => { + strictEqual(state.audience, "default-audience"); + api.setAudience("custom-audience"); + strictEqual(state.audience, "custom-audience"); + }); + + await t.test("recipient", async (t) => { + strictEqual(state.recipient, "default-recipient"); + api.setRecipient("custom-recipient"); + strictEqual(state.recipient, "custom-recipient"); + }); + + await t.test("destination", async (t) => { + strictEqual(state.destination, "default-destination"); + api.setDestination("custom-destination"); + strictEqual(state.destination, "custom-destination"); + }); + + await t.test("createUpnClaim", async (t) => { + strictEqual(state.createUpnClaim, true); + api.setCreateUpnClaim(false); + strictEqual(state.createUpnClaim, false); + }); + + await t.test("passthroughClaimsWithNoMapping", async (t) => { + strictEqual(state.passthroughClaimsWithNoMapping, true); + api.setPassthroughClaimsWithNoMapping(false); + strictEqual(state.passthroughClaimsWithNoMapping, false); + }); + + await t.test("mapUnknownClaimsAsIs", async (t) => { + strictEqual(state.mapUnknownClaimsAsIs, false); + api.setMapUnknownClaimsAsIs(true); + strictEqual(state.mapUnknownClaimsAsIs, true); + }); + + await t.test("mapIdentities", async (t) => { + strictEqual(state.mapIdentities, true); + api.setMapIdentities(false); + strictEqual(state.mapIdentities, false); + }); + + await t.test("signResponse", async (t) => { + strictEqual(state.signResponse, false); + api.setSignResponse(true); + strictEqual(state.signResponse, true); + }); + + await t.test("includeAttributeNameFormat", async (t) => { + strictEqual(state.includeAttributeNameFormat, true); + api.setIncludeAttributeNameFormat(false); + strictEqual(state.includeAttributeNameFormat, false); + }); + + await t.test("typedAttributes", async (t) => { + strictEqual(state.typedAttributes, true); + api.setTypedAttributes(false); + strictEqual(state.typedAttributes, false); + }); + + await t.test("lifetimeInSeconds", async (t) => { + strictEqual(state.lifetimeInSeconds, 3600); + api.setLifetimeInSeconds(42); + strictEqual(state.lifetimeInSeconds, 42); + }); + + await t.test("nameIdentifierFormat", async (t) => { + const { build, state } = samlResponseMock("Another Flow"); + const api = build(baseApi); + + strictEqual( + state.nameIdentifierFormat, + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + ); + + api.setNameIdentifierFormat("custom-format"); + strictEqual(state.nameIdentifierFormat, "custom-format"); + }); + + await t.test("nameIdentifierProbes", async (t) => { + const { build, state } = samlResponseMock("Another Flow"); + const api = build(baseApi); + + deepStrictEqual(state.nameIdentifierProbes, [ + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", + ]); + + api.setNameIdentifierProbes([ + "custom-probe-a", + "custom-probe-b", + ]); + + deepStrictEqual(state.nameIdentifierProbes, [ + "custom-probe-a", + "custom-probe-b", + ]); + }); + + await t.test("authnContextClassRef", async (t) => { + const { build, state } = samlResponseMock("Another Flow"); + const api = build(baseApi); + + strictEqual( + state.authnContextClassRef, + "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified" + ); + + api.setAuthnContextClassRef( + "custom-authn-context-class-ref" + ); + + strictEqual( + state.authnContextClassRef, + "custom-authn-context-class-ref" + ); + }); +}); From 4f183787bff74c57bdc6e15e0042b4fbcd234baa Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 10:44:19 +1200 Subject: [PATCH 09/13] Extract mock validation api --- src/mock/api/post-login.ts | 13 +++++-------- src/mock/api/validation.ts | 16 ++++++++++++++++ src/test/api/validation.test.ts | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 src/mock/api/validation.ts create mode 100644 src/test/api/validation.test.ts diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index f7e942a..54f3d78 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -12,6 +12,7 @@ import { multifactorMock } from "./multifactor"; import { redirectMock } from "./redirect"; import { userMock } from "./user"; import { samlResponseMock } from "./saml-response"; +import { validationMock } from "./validation"; export interface PostLoginOptions { user?: Auth0.User; @@ -106,6 +107,7 @@ export function postLogin({ const redirect = redirectMock("PostLogin", { now, request: requestValue, user: userValue }); const userApiMock = userMock("PostLogin", { user: userValue }); const samlResponse = samlResponseMock("PostLogin"); + const validation = validationMock("PostLogin"); const state: PostLoginState = { user: userApiMock.state, @@ -116,9 +118,7 @@ export function postLogin({ idToken: idToken.state, multifactor: multifactor.state, samlResponse: samlResponse.state, - validation: { - error: null, - }, + validation: validation.state, redirect: redirect.state, }; @@ -163,11 +163,8 @@ export function postLogin({ return userApiMock.build(api); }, - validation: { - error: (code, message) => { - state.validation.error = { code, message }; - return api; - }, + get validation() { + return validation.build(api); }, }; diff --git a/src/mock/api/validation.ts b/src/mock/api/validation.ts new file mode 100644 index 0000000..220c28c --- /dev/null +++ b/src/mock/api/validation.ts @@ -0,0 +1,16 @@ +import { error } from "console"; + +export function validationMock(flow: string) { + const state = { + error: null as null | { code: string; message: string }, + }; + + const build = (api: T) => ({ + error: (code: string, message: string) => { + state.error = { code, message }; + return api; + }, + }); + + return { state, build }; +} diff --git a/src/test/api/validation.test.ts b/src/test/api/validation.test.ts new file mode 100644 index 0000000..284cc73 --- /dev/null +++ b/src/test/api/validation.test.ts @@ -0,0 +1,20 @@ +import test from "node:test"; +import { strictEqual, deepStrictEqual } from "node:assert"; +import { validationMock } from "../../mock/api/validation"; + +test("Validation", async (t) => { + await t.test("error", async (t) => { + const baseApi = Symbol("Base API"); + const { state, build } = validationMock("Another Flow"); + const api = build(baseApi); + + strictEqual(state.error, null); + + strictEqual(api.error("E_KABOOM", "Something went wrong"), baseApi); + + deepStrictEqual(state.error, { + code: "E_KABOOM", + message: "Something went wrong", + }); + }); +}); From 6b779b87d43ad815b9de7d3d92c94b31df6242ec Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 10:51:58 +1200 Subject: [PATCH 10/13] Extract mock rules api --- src/mock/api/post-login.ts | 8 ++++---- src/mock/api/rules.ts | 9 +++++++++ src/test/api/rules.test.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/mock/api/rules.ts create mode 100644 src/test/api/rules.test.ts diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index 54f3d78..b7893c7 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -13,6 +13,7 @@ import { redirectMock } from "./redirect"; import { userMock } from "./user"; import { samlResponseMock } from "./saml-response"; import { validationMock } from "./validation"; +import { rulesMock } from "./rules"; export interface PostLoginOptions { user?: Auth0.User; @@ -108,6 +109,7 @@ export function postLogin({ const userApiMock = userMock("PostLogin", { user: userValue }); const samlResponse = samlResponseMock("PostLogin"); const validation = validationMock("PostLogin"); + const rules = rulesMock("PostLogin", { executedRules }); const state: PostLoginState = { user: userApiMock.state, @@ -149,10 +151,8 @@ export function postLogin({ return redirect.build(api); }, - rules: { - wasExecuted: (ruleId) => { - return executedRules.includes(ruleId); - }, + get rules() { + return rules.build(api); }, get samlResponse() { diff --git a/src/mock/api/rules.ts b/src/mock/api/rules.ts new file mode 100644 index 0000000..70ae4ee --- /dev/null +++ b/src/mock/api/rules.ts @@ -0,0 +1,9 @@ +export function rulesMock(flow: string, { executedRules = [] }: { executedRules?: string[] } = {}) { + const build = (api: T) => ({ + wasExecuted: (ruleId: string) => { + return executedRules.includes(ruleId); + }, + }); + + return { build }; +} diff --git a/src/test/api/rules.test.ts b/src/test/api/rules.test.ts new file mode 100644 index 0000000..5cbf21f --- /dev/null +++ b/src/test/api/rules.test.ts @@ -0,0 +1,14 @@ +import test from "node:test"; +import { strictEqual, deepStrictEqual } from "node:assert"; +import { rulesMock } from "../../mock/api/rules"; + +test("Rules", async (t) => { + await t.test("wasExecuted", async (t) => { + const baseApi = Symbol("Base API"); + const { build } = rulesMock("Another Flow", { executedRules: ["rule-42"] }); + const api = build(baseApi); + + strictEqual(api.wasExecuted("rule-42"), true); + strictEqual(api.wasExecuted("some-not-executed-rule"), false); + }); +}); From 1da719d4ec2cbf106fa2ab575413ff02f42272d0 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 10:52:22 +1200 Subject: [PATCH 11/13] Remove unused imports --- src/mock/api/credentials-exchange.ts | 1 - src/mock/api/post-login.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/mock/api/credentials-exchange.ts b/src/mock/api/credentials-exchange.ts index 88921f7..94e06ee 100644 --- a/src/mock/api/credentials-exchange.ts +++ b/src/mock/api/credentials-exchange.ts @@ -1,6 +1,5 @@ import Auth0 from "../../types"; import { cache as mockCache } from "./cache"; -import { request as mockRequest } from "../request"; import { accessTokenMock } from "./access-token"; import { accessMock } from "./access"; diff --git a/src/mock/api/post-login.ts b/src/mock/api/post-login.ts index b7893c7..1b03f6e 100644 --- a/src/mock/api/post-login.ts +++ b/src/mock/api/post-login.ts @@ -2,8 +2,6 @@ import Auth0, { Factor, MultifactorEnableOptions } from "../../types"; import { cache as mockCache } from "./cache"; import { user as mockUser } from "../user"; import { request as mockRequest } from "../request"; -import { ok } from "node:assert"; -import { encodeHS256JWT, signHS256 } from "../../jwt/hs256"; import { accessTokenMock } from "./access-token"; import { accessMock } from "./access"; import { authenticationMock, FactorList } from "./authentication"; From 572dc6e2b991fa730992ae1eb7ecfa70b97c0799 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 10:53:12 +1200 Subject: [PATCH 12/13] Update examples --- examples/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/package-lock.json b/examples/package-lock.json index 0d36d7c..90544d4 100644 --- a/examples/package-lock.json +++ b/examples/package-lock.json @@ -13,7 +13,7 @@ }, "..": { "name": "@kilterset/auth0-actions-testing", - "version": "0.0.4", + "version": "0.1.0", "license": "ISC", "dependencies": { "@types/chance": "^1.1.6", From 042fc6f8c56f84d9299fa956b9c1832d87ee1321 Mon Sep 17 00:00:00 2001 From: Matthew Wratt Date: Thu, 11 Apr 2024 11:35:05 +1200 Subject: [PATCH 13/13] Fix failing test --- src/test/api/credentials-exchange.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/api/credentials-exchange.test.ts b/src/test/api/credentials-exchange.test.ts index 548add3..5433cea 100644 --- a/src/test/api/credentials-exchange.test.ts +++ b/src/test/api/credentials-exchange.test.ts @@ -14,9 +14,10 @@ test("Credentials Exchange API", async (t) => { ); deepStrictEqual(state.access, { - denied: true, - code: "invalid_request", - reason: "Only cool kids allowed", + denied: { + code: "invalid_request", + reason: "Only cool kids allowed", + } }); });