Skip to content

Commit 906005c

Browse files
committed
apply suggested updates
1 parent 4bfd005 commit 906005c

File tree

2 files changed

+56
-22
lines changed

2 files changed

+56
-22
lines changed

src/client/auth.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
auth,
1111
type OAuthClientProvider,
1212
} from "./auth.js";
13-
import { OAuthMetadata } from 'src/shared/auth.js';
13+
import { OAuthMetadata } from '../shared/auth.js';
1414

1515
// Mock fetch globally
1616
const mockFetch = jest.fn();
@@ -1615,6 +1615,11 @@ describe("OAuth Authorization", () => {
16151615
token_endpoint_auth_methods_supported: ["none"],
16161616
};
16171617

1618+
const metadataWithAllBuiltinMethods = {
1619+
...metadataWithBasicOnly,
1620+
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
1621+
};
1622+
16181623
it("uses HTTP Basic authentication when client_secret_basic is supported", async () => {
16191624
mockFetch.mockResolvedValueOnce({
16201625
ok: true,
@@ -1639,8 +1644,8 @@ describe("OAuth Authorization", () => {
16391644
expect(authHeader).toBe(expected);
16401645

16411646
const body = request.body as URLSearchParams;
1642-
expect(body.get("client_id")).toBeNull(); // should not be in body
1643-
expect(body.get("client_secret")).toBeNull(); // should not be in body
1647+
expect(body.get("client_id")).toBeNull();
1648+
expect(body.get("client_secret")).toBeNull();
16441649
});
16451650

16461651
it("includes credentials in request body when client_secret_post is supported", async () => {
@@ -1669,6 +1674,35 @@ describe("OAuth Authorization", () => {
16691674
expect(body.get("client_secret")).toBe("secret123");
16701675
});
16711676

1677+
it("it picks client_secret_basic when all builtin methods are supported", async () => {
1678+
mockFetch.mockResolvedValueOnce({
1679+
ok: true,
1680+
status: 200,
1681+
json: async () => validTokens,
1682+
});
1683+
1684+
const tokens = await exchangeAuthorization("https://auth.example.com", {
1685+
metadata: metadataWithAllBuiltinMethods,
1686+
clientInformation: validClientInfo,
1687+
authorizationCode: "code123",
1688+
redirectUri: "http://localhost:3000/callback",
1689+
codeVerifier: "verifier123",
1690+
});
1691+
1692+
expect(tokens).toEqual(validTokens);
1693+
const request = mockFetch.mock.calls[0][1];
1694+
1695+
// Check Authorization header - should use Basic auth as it's the most secure
1696+
const authHeader = request.headers.get("Authorization");
1697+
const expected = "Basic " + btoa("client123:secret123");
1698+
expect(authHeader).toBe(expected);
1699+
1700+
// Credentials should not be in body when using Basic auth
1701+
const body = request.body as URLSearchParams;
1702+
expect(body.get("client_id")).toBeNull();
1703+
expect(body.get("client_secret")).toBeNull();
1704+
});
1705+
16721706
it("uses public client authentication when none method is specified", async () => {
16731707
mockFetch.mockResolvedValueOnce({
16741708
ok: true,

src/client/auth.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ export interface OAuthClientProvider {
8686
* - Adding custom headers for proprietary authentication schemes
8787
* - Implementing client assertion-based authentication (e.g., JWT bearer tokens)
8888
*
89-
* @param url - The token endpoint URL being called
9089
* @param headers - The request headers (can be modified to add authentication)
9190
* @param params - The request body parameters (can be modified to add credentials)
91+
* @param url - The token endpoint URL being called
92+
* @param metadata - Optional OAuth metadata for the server, which may include supported authentication methods
9293
*/
9394
addClientAuthentication?(headers: Headers, params: URLSearchParams, url: string | URL, metadata?: OAuthMetadata): void | Promise<void>;
9495

@@ -110,6 +111,8 @@ export class UnauthorizedError extends Error {
110111
}
111112
}
112113

114+
type ClientAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
115+
113116
/**
114117
* Determines the best client authentication method to use based on server support and client configuration.
115118
*
@@ -125,8 +128,8 @@ export class UnauthorizedError extends Error {
125128
function selectClientAuthMethod(
126129
clientInformation: OAuthClientInformation,
127130
supportedMethods: string[]
128-
): string {
129-
const hasClientSecret = !!clientInformation.client_secret;
131+
): ClientAuthMethod {
132+
const hasClientSecret = clientInformation.client_secret !== undefined;
130133

131134
// If server doesn't specify supported methods, use RFC 6749 defaults
132135
if (supportedMethods.length === 0) {
@@ -165,29 +168,26 @@ function selectClientAuthMethod(
165168
* @throws {Error} When required credentials are missing
166169
*/
167170
function applyClientAuthentication(
168-
method: string,
171+
method: ClientAuthMethod,
169172
clientInformation: OAuthClientInformation,
170173
headers: Headers,
171174
params: URLSearchParams
172175
): void {
173176
const { client_id, client_secret } = clientInformation;
174177

175-
if (method === "client_secret_basic") {
176-
applyBasicAuth(client_id, client_secret, headers);
177-
return;
178-
}
179-
180-
if (method === "client_secret_post") {
181-
applyPostAuth(client_id, client_secret, params);
182-
return;
183-
}
184-
185-
if (method === "none") {
186-
applyPublicAuth(client_id, params);
187-
return;
178+
switch (method) {
179+
case "client_secret_basic":
180+
applyBasicAuth(client_id, client_secret, headers);
181+
return;
182+
case "client_secret_post":
183+
applyPostAuth(client_id, client_secret, params);
184+
return;
185+
case "none":
186+
applyPublicAuth(client_id, params);
187+
return;
188+
default:
189+
throw new Error(`Unsupported client authentication method: ${method}`);
188190
}
189-
190-
throw new Error(`Unsupported client authentication method: ${method}`);
191191
}
192192

193193
/**

0 commit comments

Comments
 (0)