diff --git a/.gitignore b/.gitignore index 3030104d..a8e3b3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ dist-ssr *.sw? coverage -api_ \ No newline at end of file +api_ 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 e5ee9bbe..7fe20174 100644 --- a/lib/utils/exchangeAuthCode.test.ts +++ b/lib/utils/exchangeAuthCode.test.ts @@ -618,4 +618,69 @@ describe("exchangeAuthCode", () => { ), }); }); + + it("sends a sanitized redirect_uri so /token matches the /authorize value", 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/", + }); + + const [, requestInit] = fetchMock.mock.calls.at(-1)!; + const body = requestInit?.body as URLSearchParams; + + 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 d1dfb52a..2e7818ef 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; @@ -28,6 +29,7 @@ interface ExchangeAuthCodeParams { redirectURL: string; autoRefresh?: boolean; onRefresh?: (data: RefreshTokenResult) => void; + disableUrlSanitization?: boolean; } type ExchangeAuthCodeResultSuccess = { @@ -72,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"); @@ -143,7 +146,9 @@ export const exchangeAuthCode = async ({ code, code_verifier: codeVerifier, grant_type: "authorization_code", - redirect_uri: redirectURL, + redirect_uri: disableUrlSanitization + ? redirectURL + : sanitizeUrl(redirectURL), }); if (clientSecret) {