Skip to content

Commit

Permalink
Extract mock SAML response api
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-wratt committed Apr 10, 2024
1 parent c8811b0 commit 6cc1293
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 56 deletions.
62 changes: 6 additions & 56 deletions src/mock/api/post-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -207,7 +155,9 @@ export function postLogin({
},
},

samlResponse,
get samlResponse() {
return samlResponse.build(api);
},

get user() {
return userApiMock.build(api);
Expand Down
123 changes: 123 additions & 0 deletions src/mock/api/saml-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
type SamlAttributeValue =
| string
| number
| boolean
| null
| Array<string | number | boolean>;

interface SamlResponseState {
attributes: Record<string, SamlAttributeValue>;
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 = <T>(api: T) => samlResponse;

return { state, build };
}
140 changes: 140 additions & 0 deletions src/test/api/saml-response.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import test from "node:test";
import { strictEqual, deepStrictEqual, throws, ok } from "node:assert";
import { samlResponseMock } from "../../mock/api/saml-response";
import { request, user } from "../../mock";
import { encodeHS256JWT } from "../../jwt/hs256";

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"
);
});
});

0 comments on commit 6cc1293

Please sign in to comment.