Skip to content

Commit 6d60fc1

Browse files
committed
feature. Support client_secret_basic in token exchange
1 parent d5c996b commit 6d60fc1

File tree

2 files changed

+80
-17
lines changed

2 files changed

+80
-17
lines changed

src/client/auth.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,4 +808,58 @@ describe("OAuth Authorization", () => {
808808
);
809809
});
810810
});
811+
812+
describe("exchangeAuthorization with client_secret_basic", () => {
813+
const validTokens = {
814+
access_token: "access123",
815+
token_type: "Bearer",
816+
expires_in: 3600,
817+
refresh_token: "refresh123",
818+
};
819+
820+
const validClientInfo = {
821+
client_id: "client123",
822+
client_secret: "secret123",
823+
redirect_uris: ["http://localhost:3000/callback"],
824+
client_name: "Test Client",
825+
};
826+
827+
const metadataWithBasicOnly = {
828+
issuer: "https://auth.example.com",
829+
authorization_endpoint: "https://auth.example.com/auth",
830+
token_endpoint: "https://auth.example.com/token",
831+
response_types_supported: ["code"],
832+
code_challenge_methods_supported: ["S256"],
833+
token_endpoint_auth_methods_supported: ["client_secret_basic"],
834+
};
835+
836+
it("sends credentials in Authorization header when client_secret_basic is supported", async () => {
837+
mockFetch.mockResolvedValueOnce({
838+
ok: true,
839+
status: 200,
840+
json: async () => validTokens,
841+
});
842+
843+
const tokens = await exchangeAuthorization("https://auth.example.com", {
844+
metadata: metadataWithBasicOnly,
845+
clientInformation: validClientInfo,
846+
authorizationCode: "code123",
847+
codeVerifier: "verifier123",
848+
redirectUri: "http://localhost:3000/callback",
849+
});
850+
851+
expect(tokens).toEqual(validTokens);
852+
const request = mockFetch.mock.calls[0][1];
853+
854+
// Check Authorization header
855+
const authHeader = request.headers["Authorization"];
856+
const expected = "Basic " + Buffer.from("client123:secret123").toString("base64");
857+
expect(authHeader).toBe(expected);
858+
859+
const body = request.body as URLSearchParams;
860+
expect(body.get("client_id")).toBeNull(); // should not be in body
861+
expect(body.get("client_secret")).toBeNull(); // should not be in body
862+
});
863+
});
864+
811865
});

src/client/auth.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -389,40 +389,49 @@ export async function exchangeAuthorization(
389389
): Promise<OAuthTokens> {
390390
const grantType = "authorization_code";
391391

392-
let tokenUrl: URL;
393-
if (metadata) {
394-
tokenUrl = new URL(metadata.token_endpoint);
392+
const tokenUrl = metadata?.token_endpoint
393+
? new URL(metadata.token_endpoint)
394+
: new URL("/token", authorizationServerUrl);
395395

396-
if (
397-
metadata.grant_types_supported &&
396+
if (
397+
metadata?.grant_types_supported &&
398398
!metadata.grant_types_supported.includes(grantType)
399-
) {
400-
throw new Error(
399+
) {
400+
throw new Error(
401401
`Incompatible auth server: does not support grant type ${grantType}`,
402-
);
403-
}
404-
} else {
405-
tokenUrl = new URL("/token", authorizationServerUrl);
402+
);
406403
}
407404

405+
const headers: HeadersInit = {
406+
"Content-Type": "application/x-www-form-urlencoded",
407+
};
408+
408409
// Exchange code for tokens
409410
const params = new URLSearchParams({
410411
grant_type: grantType,
411-
client_id: clientInformation.client_id,
412412
code: authorizationCode,
413413
code_verifier: codeVerifier,
414414
redirect_uri: String(redirectUri),
415415
});
416416

417-
if (clientInformation.client_secret) {
418-
params.set("client_secret", clientInformation.client_secret);
417+
const { client_id, client_secret } = clientInformation;
418+
const supportedMethods =
419+
metadata?.token_endpoint_auth_methods_supported ?? [];
420+
421+
const useBasicAuth = !!client_secret && supportedMethods.includes("client_secret_basic");
422+
423+
if (client_secret && useBasicAuth) {
424+
headers["Authorization"] = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
425+
} else {
426+
params.set("client_id", client_id);
427+
if (client_secret) {
428+
params.set("client_secret", client_secret);
429+
}
419430
}
420431

421432
const response = await fetch(tokenUrl, {
422433
method: "POST",
423-
headers: {
424-
"Content-Type": "application/x-www-form-urlencoded",
425-
},
434+
headers,
426435
body: params,
427436
});
428437

0 commit comments

Comments
 (0)