From 21806caa057ef5d54ecb37238bfba58d1c29978a Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Wed, 17 Jun 2026 11:13:46 -0400 Subject: [PATCH] fix(oidc): correctly handle client_secret_basic fallback --- app/server/oidc/provider.ts | 7 ++++--- tests/unit/oidc/provider.test.ts | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/server/oidc/provider.ts b/app/server/oidc/provider.ts index 6cf3bee..58ae065 100644 --- a/app/server/oidc/provider.ts +++ b/app/server/oidc/provider.ts @@ -451,14 +451,15 @@ export function createOidcService(initialConfig: OidcConfig): OidcService { body: URLSearchParams, method: "client_secret_basic" | "client_secret_post", ): Promise> { + const requestBody = new URLSearchParams(body); const headers: Record = { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }; if (method === "client_secret_post") { - body.set("client_id", config.clientId); - body.set("client_secret", config.clientSecret); + requestBody.set("client_id", config.clientId); + requestBody.set("client_secret", config.clientSecret); } else { const credentials = btoa( `${encodeURIComponent(config.clientId)}:${encodeURIComponent(config.clientSecret)}`, @@ -472,7 +473,7 @@ export function createOidcService(initialConfig: OidcConfig): OidcService { response = await fetch(tokenEndpoint, { method: "POST", headers, - body: body.toString(), + body: requestBody.toString(), signal: AbortSignal.timeout(10_000), }); } catch (cause) { diff --git a/tests/unit/oidc/provider.test.ts b/tests/unit/oidc/provider.test.ts index 242ec13..efd1ad8 100644 --- a/tests/unit/oidc/provider.test.ts +++ b/tests/unit/oidc/provider.test.ts @@ -771,6 +771,7 @@ describe("handleCallback", () => { const idToken = await signIdToken({ sub: "user-123", name: "Test" }, flowState.nonce); let callCount = 0; + let retryBody = ""; tokenHandler = async (req, res) => { callCount++; const body = await readBody(req); @@ -782,6 +783,8 @@ describe("handleCallback", () => { return; } + retryBody = body; + res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ @@ -798,6 +801,7 @@ describe("handleCallback", () => { expect(result.ok).toBe(true); expect(callCount).toBe(2); + expect(new URLSearchParams(retryBody).has("client_secret")).toBe(false); }); test("token exchange uses client_secret_post by default", async () => { @@ -846,8 +850,10 @@ describe("handleCallback", () => { const idToken = await signIdToken({ sub: "user-123", name: "Test" }, flowState.nonce); let receivedAuth: string | undefined; + let receivedBody = ""; tokenHandler = async (req, res) => { receivedAuth = req.headers.authorization; + receivedBody = await readBody(req); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ access_token: "at", id_token: idToken, token_type: "Bearer" })); }; @@ -858,6 +864,7 @@ describe("handleCallback", () => { expect(receivedAuth).toBeDefined(); expect(receivedAuth!.startsWith("Basic ")).toBe(true); + expect(new URLSearchParams(receivedBody).has("client_secret")).toBe(false); }); });