Skip to content

Commit 874aa27

Browse files
authored
fix: Prefer the token_endpoint_auth_method response from DCR registration (#1022)
1 parent e74a358 commit 874aa27

File tree

4 files changed

+47
-11
lines changed

4 files changed

+47
-11
lines changed

src/client/auth.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
discoverOAuthProtectedResourceMetadata,
1111
extractResourceMetadataUrl,
1212
auth,
13-
type OAuthClientProvider
13+
type OAuthClientProvider,
14+
selectClientAuthMethod
1415
} from './auth.js';
1516
import { ServerError } from '../server/auth/errors.js';
1617
import { AuthorizationServerMetadata } from '../shared/auth.js';
@@ -881,6 +882,25 @@ describe('OAuth Authorization', () => {
881882
});
882883
});
883884

885+
describe('selectClientAuthMethod', () => {
886+
it('selects the correct client authentication method from client information', () => {
887+
const clientInfo = {
888+
client_id: 'test-client-id',
889+
client_secret: 'test-client-secret',
890+
token_endpoint_auth_method: 'client_secret_basic'
891+
};
892+
const supportedMethods = ['client_secret_post', 'client_secret_basic', 'none'];
893+
const authMethod = selectClientAuthMethod(clientInfo, supportedMethods);
894+
expect(authMethod).toBe('client_secret_basic');
895+
});
896+
it('selects the correct client authentication method from supported methods', () => {
897+
const clientInfo = { client_id: 'test-client-id' };
898+
const supportedMethods = ['client_secret_post', 'client_secret_basic', 'none'];
899+
const authMethod = selectClientAuthMethod(clientInfo, supportedMethods);
900+
expect(authMethod).toBe('none');
901+
});
902+
});
903+
884904
describe('startAuthorization', () => {
885905
const validMetadata = {
886906
issuer: 'https://auth.example.com',

src/client/auth.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { LATEST_PROTOCOL_VERSION } from '../types.js';
33
import {
44
OAuthClientMetadata,
55
OAuthClientInformation,
6+
OAuthClientInformationMixed,
67
OAuthTokens,
78
OAuthMetadata,
89
OAuthClientInformationFull,
@@ -56,7 +57,7 @@ export interface OAuthClientProvider {
5657
* server, or returns `undefined` if the client is not registered with the
5758
* server.
5859
*/
59-
clientInformation(): OAuthClientInformation | undefined | Promise<OAuthClientInformation | undefined>;
60+
clientInformation(): OAuthClientInformationMixed | undefined | Promise<OAuthClientInformationMixed | undefined>;
6061

6162
/**
6263
* If implemented, this permits the OAuth client to dynamically register with
@@ -66,7 +67,7 @@ export interface OAuthClientProvider {
6667
* This method is not required to be implemented if client information is
6768
* statically known (e.g., pre-registered).
6869
*/
69-
saveClientInformation?(clientInformation: OAuthClientInformationFull): void | Promise<void>;
70+
saveClientInformation?(clientInformation: OAuthClientInformationMixed): void | Promise<void>;
7071

7172
/**
7273
* Loads any existing OAuth tokens for the current session, or returns
@@ -149,6 +150,10 @@ export class UnauthorizedError extends Error {
149150

150151
type ClientAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
151152

153+
function isClientAuthMethod(method: string): method is ClientAuthMethod {
154+
return ['client_secret_basic', 'client_secret_post', 'none'].includes(method);
155+
}
156+
152157
const AUTHORIZATION_CODE_RESPONSE_TYPE = 'code';
153158
const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
154159

@@ -164,14 +169,24 @@ const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
164169
* @param supportedMethods - Authentication methods supported by the authorization server
165170
* @returns The selected authentication method
166171
*/
167-
function selectClientAuthMethod(clientInformation: OAuthClientInformation, supportedMethods: string[]): ClientAuthMethod {
172+
export function selectClientAuthMethod(clientInformation: OAuthClientInformationMixed, supportedMethods: string[]): ClientAuthMethod {
168173
const hasClientSecret = clientInformation.client_secret !== undefined;
169174

170175
// If server doesn't specify supported methods, use RFC 6749 defaults
171176
if (supportedMethods.length === 0) {
172177
return hasClientSecret ? 'client_secret_post' : 'none';
173178
}
174179

180+
// Prefer the method returned by the server during client registration if valid and supported
181+
if (
182+
'token_endpoint_auth_method' in clientInformation &&
183+
clientInformation.token_endpoint_auth_method &&
184+
isClientAuthMethod(clientInformation.token_endpoint_auth_method) &&
185+
supportedMethods.includes(clientInformation.token_endpoint_auth_method)
186+
) {
187+
return clientInformation.token_endpoint_auth_method;
188+
}
189+
175190
// Try methods in priority order (most secure first)
176191
if (hasClientSecret && supportedMethods.includes('client_secret_basic')) {
177192
return 'client_secret_basic';
@@ -793,7 +808,7 @@ export async function startAuthorization(
793808
resource
794809
}: {
795810
metadata?: AuthorizationServerMetadata;
796-
clientInformation: OAuthClientInformation;
811+
clientInformation: OAuthClientInformationMixed;
797812
redirectUrl: string | URL;
798813
scope?: string;
799814
state?: string;
@@ -876,7 +891,7 @@ export async function exchangeAuthorization(
876891
fetchFn
877892
}: {
878893
metadata?: AuthorizationServerMetadata;
879-
clientInformation: OAuthClientInformation;
894+
clientInformation: OAuthClientInformationMixed;
880895
authorizationCode: string;
881896
codeVerifier: string;
882897
redirectUri: string | URL;
@@ -955,7 +970,7 @@ export async function refreshAuthorization(
955970
fetchFn
956971
}: {
957972
metadata?: AuthorizationServerMetadata;
958-
clientInformation: OAuthClientInformation;
973+
clientInformation: OAuthClientInformationMixed;
959974
refreshToken: string;
960975
resource?: URL;
961976
addClientAuthentication?: OAuthClientProvider['addClientAuthentication'];

src/examples/client/simpleOAuthClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { URL } from 'node:url';
66
import { exec } from 'node:child_process';
77
import { Client } from '../../client/index.js';
88
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
9-
import { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '../../shared/auth.js';
9+
import { OAuthClientInformationMixed, OAuthClientMetadata, OAuthTokens } from '../../shared/auth.js';
1010
import { CallToolRequest, ListToolsRequest, CallToolResultSchema, ListToolsResultSchema } from '../../types.js';
1111
import { OAuthClientProvider, UnauthorizedError } from '../../client/auth.js';
1212

@@ -20,7 +20,7 @@ const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
2020
* In production, you should persist tokens securely
2121
*/
2222
class InMemoryOAuthClientProvider implements OAuthClientProvider {
23-
private _clientInformation?: OAuthClientInformationFull;
23+
private _clientInformation?: OAuthClientInformationMixed;
2424
private _tokens?: OAuthTokens;
2525
private _codeVerifier?: string;
2626

@@ -46,11 +46,11 @@ class InMemoryOAuthClientProvider implements OAuthClientProvider {
4646
return this._clientMetadata;
4747
}
4848

49-
clientInformation(): OAuthClientInformation | undefined {
49+
clientInformation(): OAuthClientInformationMixed | undefined {
5050
return this._clientInformation;
5151
}
5252

53-
saveClientInformation(clientInformation: OAuthClientInformationFull): void {
53+
saveClientInformation(clientInformation: OAuthClientInformationMixed): void {
5454
this._clientInformation = clientInformation;
5555
}
5656

src/shared/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export type OAuthErrorResponse = z.infer<typeof OAuthErrorResponseSchema>;
226226
export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
227227
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
228228
export type OAuthClientInformationFull = z.infer<typeof OAuthClientInformationFullSchema>;
229+
export type OAuthClientInformationMixed = OAuthClientInformation | OAuthClientInformationFull;
229230
export type OAuthClientRegistrationError = z.infer<typeof OAuthClientRegistrationErrorSchema>;
230231
export type OAuthTokenRevocationRequest = z.infer<typeof OAuthTokenRevocationRequestSchema>;
231232
export type OAuthProtectedResourceMetadata = z.infer<typeof OAuthProtectedResourceMetadataSchema>;

0 commit comments

Comments
 (0)