Skip to content

Commit

Permalink
Redirect API: make consistent
Browse files Browse the repository at this point in the history
PostLogin had `state.redirect.target` whereas PostChallenge had `state.redirect`.
  • Loading branch information
Aupajo authored and matt-wratt committed Jul 1, 2024
1 parent 34cb1a3 commit 0d874b2
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 138 deletions.
8 changes: 4 additions & 4 deletions examples/Login/redirect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,26 @@ test("redirect and continue with signed data", async (t) => {
const { redirect } = action;

strict(
redirect.target.queryParams.theme,
redirect.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.target.url.origin,
redirect.url.origin,
"https://example.com",
"Unexpected redirect URL origin"
);

strictEqual(
redirect.target.url.pathname,
redirect.url.pathname,
"/sandwich-preferences",
"Unexpected redirect URL path"
);

// Test the signed JWT data payload
const { session_token } = redirect.target.queryParams;
const { session_token } = redirect.queryParams;

const decoded = jwt.decodeJWTPayload(session_token);

Expand Down
103 changes: 11 additions & 92 deletions src/mock/api/post-challenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import Auth0, { Factor } 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 { redirectMock } from "./redirect";

export interface PostChallengeOptions {
user?: Auth0.User;
Expand Down Expand Up @@ -38,8 +37,12 @@ export function postChallenge({
const apiCache = mockCache(cache);
const userValue = user ?? mockUser();
const requestValue = request ?? mockRequest();

const now = new Date(nowValue || Date.now());
const redirect = redirectMock("PostChallenge", {
now,
request: requestValue,
user: userValue,
});

const state: PostChallengeState = {
authentication: {
Expand All @@ -51,7 +54,9 @@ export function postChallenge({
user: userValue,
access: { denied: false },
cache: apiCache,
redirect: null,
get redirect() {
return redirect.state.target;
},
};

const api: Auth0.API.PostChallenge = {
Expand Down Expand Up @@ -81,94 +86,8 @@ export function postChallenge({

cache: apiCache,

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<string, unknown>;
},
get redirect() {
return redirect.build(api);
},
};

Expand Down
8 changes: 4 additions & 4 deletions src/mock/api/post-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ export interface PostLoginState {
validation: {
error: { code: string; message: string } | null;
};
redirect: {
target: { url: URL; queryParams: Record<string, string> } | null;
};
redirect: { url: URL; queryParams: Record<string, string> } | null;
}

export function postLogin({
Expand Down Expand Up @@ -125,7 +123,9 @@ export function postLogin({
multifactor: multifactor.state,
samlResponse: samlResponse.state,
validation: validation.state,
redirect: redirect.state,
get redirect() {
return redirect.state.target;
},
};

const api: Auth0.API.PostLogin = {
Expand Down
13 changes: 10 additions & 3 deletions src/mock/api/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface RedirectMockOptions {
interface EncodeTokenOptions {
expiresInSeconds?: number | undefined;
payload: {
[key: string]: unknown;
[key: string]: unknown;
};
secret: string;
}
Expand All @@ -25,15 +25,22 @@ interface ValidateTokenOptions {
secret: string;
}

export function redirectMock(flow: string, { now: nowValue, request, user }: RedirectMockOptions) {
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) => {
encodeToken: ({
expiresInSeconds,
payload,
secret,
}: EncodeTokenOptions) => {
expiresInSeconds = expiresInSeconds ?? 900;

const claims = {
Expand Down
6 changes: 3 additions & 3 deletions src/mock/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { User } from "../../types";

export function userMock(flow: string, { user }: { user: User}) {
const state = user
export function userMock(flow: string, { user }: { user: User }) {
const state = user;

const build = <T>(api: T) => ({
setAppMetadata: (key: string, value: unknown) => {
Expand All @@ -16,5 +16,5 @@ export function userMock(flow: string, { user }: { user: User}) {
},
});

return { build, state }
return { build, state };
}
20 changes: 6 additions & 14 deletions src/test/api/post-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,17 +421,9 @@ test("PostLogin API", async (t) => {

const { redirect } = state;

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"
);
ok(redirect, "redirect not set");
deepStrictEqual(redirect.queryParams, {}, "query should be empty");
strictEqual(redirect.url.href, "https://example.com/r", "url mismatch");
});

await t.test("redirect with consolidated GET parameters", async (t) => {
Expand All @@ -446,16 +438,16 @@ test("PostLogin API", async (t) => {

const { redirect } = state;

ok(redirect.target, "redirect not set");
ok(redirect, "redirect not set");

deepStrictEqual(
redirect.target.queryParams,
redirect.queryParams,
{ bread: "rye", filling: "cheese", spread: "butter" },
"unexpected query"
);

strictEqual(
redirect.target.url.href,
redirect.url.href,
"https://example.com/?bread=rye&filling=cheese&spread=butter",
"url mismatch"
);
Expand Down
Loading

0 comments on commit 0d874b2

Please sign in to comment.