From 57abb1fdcf9bb0b6802de24160b839f03f94d6c0 Mon Sep 17 00:00:00 2001 From: Tom Foxwell Date: Tue, 12 May 2026 01:27:06 +1000 Subject: [PATCH 1/7] Sanitize redirect URL in exchangeAuthCode --- lib/utils/exchangeAuthCode.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/exchangeAuthCode.ts b/lib/utils/exchangeAuthCode.ts index d1dfb52a..8357daeb 100644 --- a/lib/utils/exchangeAuthCode.ts +++ b/lib/utils/exchangeAuthCode.ts @@ -9,6 +9,7 @@ import { import { isCustomDomain } from "."; import { clearRefreshTimer, setRefreshTimer } from "./refreshTimer"; import { isClient } from "./isClient"; +import { sanitizeUrl } from "./sanitizeUrl"; export const frameworkSettings: { framework: string; @@ -143,7 +144,7 @@ export const exchangeAuthCode = async ({ code, code_verifier: codeVerifier, grant_type: "authorization_code", - redirect_uri: redirectURL, + redirect_uri: sanitizeUrl(redirectURL), }); if (clientSecret) { From 172de778529ec8c6098a4f0cbf3fc4702fa3c6a6 Mon Sep 17 00:00:00 2001 From: Tom Foxwell Date: Tue, 12 May 2026 01:30:57 +1000 Subject: [PATCH 2/7] Add test for sanitized redirect_uri in exchangeAuthCode Add test to ensure sanitized redirect_uri matches expected value. --- lib/utils/exchangeAuthCode.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/utils/exchangeAuthCode.test.ts b/lib/utils/exchangeAuthCode.test.ts index e5ee9bbe..dd9176c8 100644 --- a/lib/utils/exchangeAuthCode.test.ts +++ b/lib/utils/exchangeAuthCode.test.ts @@ -618,4 +618,24 @@ describe("exchangeAuthCode", () => { ), }); }); + + it("sends a sanitized redirect_uri so /token matches the /authorize value", async () => { + // Setup: stash valid state + codeVerifier, mock fetch, etc. + // (use the same setup helpers as the surrounding tests) + + const urlParams = new URLSearchParams({ state: "abc", code: "xyz" }); + + await exchangeAuthCode({ + urlParams, + domain: "https://example.kinde.com", + clientId: "test-client", + // Trailing slash here is the key — must be stripped before POST. + redirectURL: "https://app.example.com/", + }); + + const [, requestInit] = (global.fetch as jest.Mock).mock.calls.at(-1); + const body = new URLSearchParams(requestInit.body as string); + + expect(body.get("redirect_uri")).toBe("https://app.example.com"); + }); }); From 44dd08ca95fe6a8755e0eee5513f9740feabb48d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 15:52:30 +0000 Subject: [PATCH 3/7] fix: repair sanitized redirect_uri test and prettier formatting The new test was missing storage setup (state + codeVerifier) and a fetchMock response, causing exchangeAuthCode to bail out early before reaching fetch. Also fixes prettier formatting in types.ts and the test file. https://claude.ai/code/session_01MfwF59xe6Rs5kyxnQuPRAR --- lib/sessionManager/types.ts | 6 +++--- lib/utils/exchangeAuthCode.test.ts | 28 ++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/sessionManager/types.ts b/lib/sessionManager/types.ts index 37429e7d..09331b27 100644 --- a/lib/sessionManager/types.ts +++ b/lib/sessionManager/types.ts @@ -48,9 +48,9 @@ export type StorageSettingsType = { onRefreshHandler?: (refreshType: RefreshType) => Promise; }; -export abstract class SessionBase - implements SessionManager -{ +export abstract class SessionBase< + V extends string = StorageKeys, +> implements SessionManager { abstract asyncStore: boolean; private listeners: Set = new Set(); private notificationScheduled = false; diff --git a/lib/utils/exchangeAuthCode.test.ts b/lib/utils/exchangeAuthCode.test.ts index dd9176c8..401730ef 100644 --- a/lib/utils/exchangeAuthCode.test.ts +++ b/lib/utils/exchangeAuthCode.test.ts @@ -620,22 +620,34 @@ describe("exchangeAuthCode", () => { }); it("sends a sanitized redirect_uri so /token matches the /authorize value", async () => { - // Setup: stash valid state + codeVerifier, mock fetch, etc. - // (use the same setup helpers as the surrounding tests) + const store = new MemoryStorage(); + setActiveStorage(store); + + await store.setItems({ + [StorageKeys.state]: "abc", + [StorageKeys.codeVerifier]: "verifier", + }); const urlParams = new URLSearchParams({ state: "abc", code: "xyz" }); - + + fetchMock.mockResponseOnce( + JSON.stringify({ + access_token: "access_token", + refresh_token: "refresh_token", + id_token: "id_token", + }), + ); + await exchangeAuthCode({ urlParams, domain: "https://example.kinde.com", clientId: "test-client", - // Trailing slash here is the key — must be stripped before POST. redirectURL: "https://app.example.com/", }); - - const [, requestInit] = (global.fetch as jest.Mock).mock.calls.at(-1); - const body = new URLSearchParams(requestInit.body as string); - + + const [, requestInit] = fetchMock.mock.calls.at(-1)!; + const body = requestInit?.body as URLSearchParams; + expect(body.get("redirect_uri")).toBe("https://app.example.com"); }); }); From ca5b6949b527eba7eeb100710344556b15e1e1d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 15:53:31 +0000 Subject: [PATCH 4/7] chore: ignore package-lock.json This file was never tracked in the project and is generated locally by npm install. https://claude.ai/code/session_01MfwF59xe6Rs5kyxnQuPRAR --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3030104d..750e1364 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +package-lock.json # Editor directories and files .vscode/* From 15dd62f3de478b3b3e1cac19b15728afc6415105 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 16:03:33 +0000 Subject: [PATCH 5/7] fix: honour disableUrlSanitization in exchangeAuthCode redirect_uri When callers pass disableUrlSanitization: true to generateAuthUrl the authorize request sends the raw redirectURL. The token exchange must use the same value; unconditionally calling sanitizeUrl() would produce a mismatched redirect_uri and cause the provider to reject the exchange. Adds the disableUrlSanitization option (default false) to ExchangeAuthCodeParams and mirrors the same conditional used in mapLoginMethodParamsForUrl. Also adds a test covering the raw-URI path. https://claude.ai/code/session_01MfwF59xe6Rs5kyxnQuPRAR --- lib/utils/exchangeAuthCode.test.ts | 33 ++++++++++++++++++++++++++++++ lib/utils/exchangeAuthCode.ts | 6 +++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/utils/exchangeAuthCode.test.ts b/lib/utils/exchangeAuthCode.test.ts index 401730ef..7fe20174 100644 --- a/lib/utils/exchangeAuthCode.test.ts +++ b/lib/utils/exchangeAuthCode.test.ts @@ -650,4 +650,37 @@ describe("exchangeAuthCode", () => { expect(body.get("redirect_uri")).toBe("https://app.example.com"); }); + + it("preserves raw redirect_uri when disableUrlSanitization is true", async () => { + const store = new MemoryStorage(); + setActiveStorage(store); + + await store.setItems({ + [StorageKeys.state]: "abc", + [StorageKeys.codeVerifier]: "verifier", + }); + + const urlParams = new URLSearchParams({ state: "abc", code: "xyz" }); + + fetchMock.mockResponseOnce( + JSON.stringify({ + access_token: "access_token", + refresh_token: "refresh_token", + id_token: "id_token", + }), + ); + + await exchangeAuthCode({ + urlParams, + domain: "https://example.kinde.com", + clientId: "test-client", + redirectURL: "https://app.example.com/", + disableUrlSanitization: true, + }); + + const [, requestInit] = fetchMock.mock.calls.at(-1)!; + const body = requestInit?.body as URLSearchParams; + + expect(body.get("redirect_uri")).toBe("https://app.example.com/"); + }); }); diff --git a/lib/utils/exchangeAuthCode.ts b/lib/utils/exchangeAuthCode.ts index 8357daeb..2e7818ef 100644 --- a/lib/utils/exchangeAuthCode.ts +++ b/lib/utils/exchangeAuthCode.ts @@ -29,6 +29,7 @@ interface ExchangeAuthCodeParams { redirectURL: string; autoRefresh?: boolean; onRefresh?: (data: RefreshTokenResult) => void; + disableUrlSanitization?: boolean; } type ExchangeAuthCodeResultSuccess = { @@ -73,6 +74,7 @@ export const exchangeAuthCode = async ({ redirectURL, autoRefresh = false, onRefresh, + disableUrlSanitization = false, }: ExchangeAuthCodeParams): Promise => { const state = urlParams.get("state"); const code = urlParams.get("code"); @@ -144,7 +146,9 @@ export const exchangeAuthCode = async ({ code, code_verifier: codeVerifier, grant_type: "authorization_code", - redirect_uri: sanitizeUrl(redirectURL), + redirect_uri: disableUrlSanitization + ? redirectURL + : sanitizeUrl(redirectURL), }); if (clientSecret) { From 9add6a71e9b724e7fa43d25471552a86f148f3c8 Mon Sep 17 00:00:00 2001 From: Tom Foxwell Date: Tue, 12 May 2026 02:59:45 +1000 Subject: [PATCH 6/7] Update .gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 750e1364..a8e3b3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ node_modules dist dist-ssr *.local -package-lock.json # Editor directories and files .vscode/* @@ -25,4 +24,4 @@ package-lock.json *.sw? coverage -api_ \ No newline at end of file +api_ From 1a77b55386207a11899b2e0209ba45612bbac3ee Mon Sep 17 00:00:00 2001 From: Tom Foxwell Date: Tue, 12 May 2026 03:00:05 +1000 Subject: [PATCH 7/7] Update .gitignore to include api_ directory