fix(oidc): correctly handle client_secret_basic fallback

This commit is contained in:
Aarnav Tale
2026-06-17 11:13:46 -04:00
parent 2f3a440de5
commit 21806caa05
2 changed files with 11 additions and 3 deletions
+4 -3
View File
@@ -451,14 +451,15 @@ export function createOidcService(initialConfig: OidcConfig): OidcService {
body: URLSearchParams, body: URLSearchParams,
method: "client_secret_basic" | "client_secret_post", method: "client_secret_basic" | "client_secret_post",
): Promise<Result<TokenResponse, OidcError>> { ): Promise<Result<TokenResponse, OidcError>> {
const requestBody = new URLSearchParams(body);
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json", Accept: "application/json",
}; };
if (method === "client_secret_post") { if (method === "client_secret_post") {
body.set("client_id", config.clientId); requestBody.set("client_id", config.clientId);
body.set("client_secret", config.clientSecret); requestBody.set("client_secret", config.clientSecret);
} else { } else {
const credentials = btoa( const credentials = btoa(
`${encodeURIComponent(config.clientId)}:${encodeURIComponent(config.clientSecret)}`, `${encodeURIComponent(config.clientId)}:${encodeURIComponent(config.clientSecret)}`,
@@ -472,7 +473,7 @@ export function createOidcService(initialConfig: OidcConfig): OidcService {
response = await fetch(tokenEndpoint, { response = await fetch(tokenEndpoint, {
method: "POST", method: "POST",
headers, headers,
body: body.toString(), body: requestBody.toString(),
signal: AbortSignal.timeout(10_000), signal: AbortSignal.timeout(10_000),
}); });
} catch (cause) { } catch (cause) {
+7
View File
@@ -771,6 +771,7 @@ describe("handleCallback", () => {
const idToken = await signIdToken({ sub: "user-123", name: "Test" }, flowState.nonce); const idToken = await signIdToken({ sub: "user-123", name: "Test" }, flowState.nonce);
let callCount = 0; let callCount = 0;
let retryBody = "";
tokenHandler = async (req, res) => { tokenHandler = async (req, res) => {
callCount++; callCount++;
const body = await readBody(req); const body = await readBody(req);
@@ -782,6 +783,8 @@ describe("handleCallback", () => {
return; return;
} }
retryBody = body;
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end( res.end(
JSON.stringify({ JSON.stringify({
@@ -798,6 +801,7 @@ describe("handleCallback", () => {
expect(result.ok).toBe(true); expect(result.ok).toBe(true);
expect(callCount).toBe(2); expect(callCount).toBe(2);
expect(new URLSearchParams(retryBody).has("client_secret")).toBe(false);
}); });
test("token exchange uses client_secret_post by default", async () => { 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); const idToken = await signIdToken({ sub: "user-123", name: "Test" }, flowState.nonce);
let receivedAuth: string | undefined; let receivedAuth: string | undefined;
let receivedBody = "";
tokenHandler = async (req, res) => { tokenHandler = async (req, res) => {
receivedAuth = req.headers.authorization; receivedAuth = req.headers.authorization;
receivedBody = await readBody(req);
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ access_token: "at", id_token: idToken, token_type: "Bearer" })); res.end(JSON.stringify({ access_token: "at", id_token: idToken, token_type: "Bearer" }));
}; };
@@ -858,6 +864,7 @@ describe("handleCallback", () => {
expect(receivedAuth).toBeDefined(); expect(receivedAuth).toBeDefined();
expect(receivedAuth!.startsWith("Basic ")).toBe(true); expect(receivedAuth!.startsWith("Basic ")).toBe(true);
expect(new URLSearchParams(receivedBody).has("client_secret")).toBe(false);
}); });
}); });