Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/client/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1587,7 +1587,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods for authorization flow
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -1657,7 +1658,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods for token exchange
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.codeVerifier as jest.Mock).mockResolvedValue('test-verifier');
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -1723,7 +1725,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods for token refresh
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.tokens as jest.Mock).mockResolvedValue({
access_token: 'old-access',
Expand Down Expand Up @@ -1789,7 +1792,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods
(providerWithCustomValidation.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(providerWithCustomValidation.tokens as jest.Mock).mockResolvedValue(undefined);
(providerWithCustomValidation.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -1844,7 +1848,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -1902,7 +1907,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -1969,7 +1975,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods for token exchange
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.codeVerifier as jest.Mock).mockResolvedValue('test-verifier');
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -2032,7 +2039,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods for token refresh
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.tokens as jest.Mock).mockResolvedValue({
access_token: 'old-access',
Expand Down Expand Up @@ -2093,7 +2101,8 @@ describe('OAuth Authorization', () => {
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
client_secret: 'test-secret',
redirect_uris: ['http://localhost:3000/callback']
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
Expand Down Expand Up @@ -2155,7 +2164,8 @@ describe('OAuth Authorization', () => {
},
clientInformation: jest.fn().mockResolvedValue({
client_id: 'client123',
client_secret: 'secret123'
client_secret: 'secret123',
redirect_uris: ['http://localhost:3000/callback']
}),
tokens: jest.fn().mockResolvedValue(undefined),
saveTokens: jest.fn(),
Expand Down
26 changes: 19 additions & 7 deletions src/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import pkceChallenge from 'pkce-challenge';
import { LATEST_PROTOCOL_VERSION } from '../types.js';
import {
OAuthClientMetadata,
OAuthClientInformation,
OAuthTokens,
OAuthMetadata,
OAuthClientInformationFull,
Expand Down Expand Up @@ -56,7 +55,7 @@ export interface OAuthClientProvider {
* server, or returns `undefined` if the client is not registered with the
* server.
*/
clientInformation(): OAuthClientInformation | undefined | Promise<OAuthClientInformation | undefined>;
clientInformation(): OAuthClientInformationFull | undefined | Promise<OAuthClientInformationFull | undefined>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type signature change worries me a bit. The OAuthClientInformation is the response from the DCR endpoint, listed here:
https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.1

All the fields are optional except for redirect_uris (which is what I think is what required a bunch of changes in tests).

this means this change will be backwards incompatible for implementations of the provider that directly return the DCR response rather than the full metadata.

saveClientInformation takes the full information type, so this might be a non issue.

to land this, we'll either want to:

  • check around for implementations to see if they commonly return the reduced type, to see the estimated impact of the breaking change
  • OR: make this backwards compatible

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I can make it backwards compatible. My only argument for changing the type is that it seems like the current type is wrong. As you pointed out, saveClientInformation() takes the OAuthClientInformationFull type and is the only way clientInformation() is set.


/**
* If implemented, this permits the OAuth client to dynamically register with
Expand Down Expand Up @@ -149,6 +148,10 @@ export class UnauthorizedError extends Error {

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

function isClientAuthMethod(method: string): method is ClientAuthMethod {
return ['client_secret_basic', 'client_secret_post', 'none'].includes(method);
}

const AUTHORIZATION_CODE_RESPONSE_TYPE = 'code';
const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';

Expand All @@ -164,14 +167,23 @@ const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
* @param supportedMethods - Authentication methods supported by the authorization server
* @returns The selected authentication method
*/
function selectClientAuthMethod(clientInformation: OAuthClientInformation, supportedMethods: string[]): ClientAuthMethod {
function selectClientAuthMethod(clientInformation: OAuthClientInformationFull, supportedMethods: string[]): ClientAuthMethod {
const hasClientSecret = clientInformation.client_secret !== undefined;

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

// Prefer the method returned by the server during client registration if valid and supported
if (
clientInformation.token_endpoint_auth_method &&
isClientAuthMethod(clientInformation.token_endpoint_auth_method) &&
supportedMethods.includes(clientInformation.token_endpoint_auth_method)
) {
return clientInformation.token_endpoint_auth_method;
}

// Try methods in priority order (most secure first)
if (hasClientSecret && supportedMethods.includes('client_secret_basic')) {
return 'client_secret_basic';
Expand Down Expand Up @@ -205,7 +217,7 @@ function selectClientAuthMethod(clientInformation: OAuthClientInformation, suppo
*/
function applyClientAuthentication(
method: ClientAuthMethod,
clientInformation: OAuthClientInformation,
clientInformation: OAuthClientInformationFull,
headers: Headers,
params: URLSearchParams
): void {
Expand Down Expand Up @@ -790,7 +802,7 @@ export async function startAuthorization(
resource
}: {
metadata?: AuthorizationServerMetadata;
clientInformation: OAuthClientInformation;
clientInformation: OAuthClientInformationFull;
redirectUrl: string | URL;
scope?: string;
state?: string;
Expand Down Expand Up @@ -873,7 +885,7 @@ export async function exchangeAuthorization(
fetchFn
}: {
metadata?: AuthorizationServerMetadata;
clientInformation: OAuthClientInformation;
clientInformation: OAuthClientInformationFull;
authorizationCode: string;
codeVerifier: string;
redirectUri: string | URL;
Expand Down Expand Up @@ -952,7 +964,7 @@ export async function refreshAuthorization(
fetchFn
}: {
metadata?: AuthorizationServerMetadata;
clientInformation: OAuthClientInformation;
clientInformation: OAuthClientInformationFull;
refreshToken: string;
resource?: URL;
addClientAuthentication?: OAuthClientProvider['addClientAuthentication'];
Expand Down
9 changes: 7 additions & 2 deletions src/client/sse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,11 @@ describe('SSEClientTransport', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
clientInformation: jest.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
clientInformation: jest.fn(() => ({
client_id: 'test-client-id',
client_secret: 'test-client-secret',
redirect_uris: ['http://localhost/callback']
})),
tokens: jest.fn(),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
Expand Down Expand Up @@ -1159,7 +1163,8 @@ describe('SSEClientTransport', () => {
const clientInfo = config.clientRegistered
? {
client_id: 'test-client-id',
client_secret: 'test-client-secret'
client_secret: 'test-client-secret',
redirect_uris: ['http://localhost/callback']
}
: undefined;

Expand Down
6 changes: 5 additions & 1 deletion src/client/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ describe('StreamableHTTPClientTransport', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
clientInformation: jest.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
clientInformation: jest.fn(() => ({
client_id: 'test-client-id',
client_secret: 'test-client-secret',
redirect_uris: ['http://localhost/callback']
})),
tokens: jest.fn(),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
Expand Down
4 changes: 2 additions & 2 deletions src/examples/client/simpleOAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { URL } from 'node:url';
import { exec } from 'node:child_process';
import { Client } from '../../client/index.js';
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
import { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '../../shared/auth.js';
import { OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '../../shared/auth.js';
import { CallToolRequest, ListToolsRequest, CallToolResultSchema, ListToolsResultSchema } from '../../types.js';
import { OAuthClientProvider, UnauthorizedError } from '../../client/auth.js';

Expand Down Expand Up @@ -46,7 +46,7 @@ class InMemoryOAuthClientProvider implements OAuthClientProvider {
return this._clientMetadata;
}

clientInformation(): OAuthClientInformation | undefined {
clientInformation(): OAuthClientInformationFull | undefined {
return this._clientInformation;
}

Expand Down