Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 5 additions & 0 deletions .changeset/spotty-cats-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/client': patch
Comment thread
NSeydoux marked this conversation as resolved.
Outdated
---

The client credentials providers now support scopes being added to the token request.
24 changes: 21 additions & 3 deletions packages/client/src/client/authExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export interface ClientCredentialsProviderOptions {
* Optional client name for metadata.
*/
clientName?: string;

/**
* Space-separated scopes values requested by the client.
*/
scope?: string;
}

/**
Expand Down Expand Up @@ -137,7 +142,8 @@ export class ClientCredentialsProvider implements OAuthClientProvider {
client_name: options.clientName ?? 'client-credentials-client',
redirect_uris: [],
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'client_secret_basic'
token_endpoint_auth_method: 'client_secret_basic',
scope: options.scope
};
}

Expand Down Expand Up @@ -213,6 +219,11 @@ export interface PrivateKeyJwtProviderOptions {
* Optional JWT lifetime in seconds (default: 300).
*/
jwtLifetimeSeconds?: number;

/**
* Space-separated scopes values requested by the client.
*/
scope?: string;
}

/**
Expand Down Expand Up @@ -246,7 +257,8 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider {
client_name: options.clientName ?? 'private-key-jwt-client',
redirect_uris: [],
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'private_key_jwt'
token_endpoint_auth_method: 'private_key_jwt',
scope: options.scope
};
this.addClientAuthentication = createPrivateKeyJwtAuth({
issuer: options.clientId,
Expand Down Expand Up @@ -321,6 +333,11 @@ export interface StaticPrivateKeyJwtProviderOptions {
* Optional client name for metadata.
*/
clientName?: string;

/**
* Space-separated scopes values requested by the client.
*/
scope?: string;
}

/**
Expand All @@ -344,7 +361,8 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider {
client_name: options.clientName ?? 'static-private-key-jwt-client',
redirect_uris: [],
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'private_key_jwt'
token_endpoint_auth_method: 'private_key_jwt',
scope: options.scope
};

const assertion = options.jwtBearerAssertion;
Expand Down
94 changes: 94 additions & 0 deletions packages/client/test/client/authExtensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
expect(tokens?.access_token).toBe('test-access-token');
});

it('sends scope in token request when ClientCredentialsProvider is configured with scope', async () => {
const provider = new ClientCredentialsProvider({
clientId: 'my-client',
clientSecret: 'my-secret',
clientName: 'test-client',
scope: 'read write'
});

expect(provider.clientMetadata.scope).toBe('read write');

const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL,
onTokenRequest: async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('scope')).toBe('read write');
}
});

const result = await auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
});

expect(result).toBe('AUTHORIZED');
});

it('authenticates using PrivateKeyJwtProvider with private_key_jwt', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
Expand Down Expand Up @@ -93,6 +122,38 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
expect(assertionFromRequest).toBeTruthy();
});

it('sends scope in token request when PrivateKeyJwtProvider is configured with scope', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
algorithm: 'HS256',
clientName: 'private-key-jwt-client',
scope: 'openid profile'
});

expect(provider.clientMetadata.scope).toBe('openid profile');

const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL,
onTokenRequest: async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('scope')).toBe('openid profile');
expect(params.get('client_assertion')).toBeTruthy();
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
}
});

const result = await auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
});

expect(result).toBe('AUTHORIZED');
});

it('fails when PrivateKeyJwtProvider is configured with an unsupported algorithm', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
Expand Down Expand Up @@ -150,6 +211,39 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
expect(tokens).toBeTruthy();
expect(tokens?.access_token).toBe('test-access-token');
});

it('sends scope in token request when StaticPrivateKeyJwtProvider is configured with scope', async () => {
const staticAssertion = 'header.payload.signature';

const provider = new StaticPrivateKeyJwtProvider({
clientId: 'static-client',
jwtBearerAssertion: staticAssertion,
clientName: 'static-private-key-jwt-client',
scope: 'api:read api:write'
});

expect(provider.clientMetadata.scope).toBe('api:read api:write');

const fetchMock = createMockOAuthFetch({
resourceServerUrl: RESOURCE_SERVER_URL,
authServerUrl: AUTH_SERVER_URL,
onTokenRequest: async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('scope')).toBe('api:read api:write');
expect(params.get('client_assertion')).toBe(staticAssertion);
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
}
});

const result = await auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
});

expect(result).toBe('AUTHORIZED');
});
});

describe('createPrivateKeyJwtAuth', () => {
Expand Down
Loading