Private key jwt scopes (#1443)
This commit is contained in:
parent
9efecc27ff
commit
4aec5f7906
3 changed files with 120 additions and 3 deletions
5
.changeset/spotty-cats-tickle.md
Normal file
5
.changeset/spotty-cats-tickle.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@modelcontextprotocol/client': minor
|
||||
---
|
||||
|
||||
The client credentials providers now support scopes being added to the token request.
|
||||
|
|
@ -112,6 +112,11 @@ export interface ClientCredentialsProviderOptions {
|
|||
* Optional client name for metadata.
|
||||
*/
|
||||
clientName?: string;
|
||||
|
||||
/**
|
||||
* Space-separated scopes values requested by the client.
|
||||
*/
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,7 +151,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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +228,11 @@ export interface PrivateKeyJwtProviderOptions {
|
|||
* Optional JWT lifetime in seconds (default: 300).
|
||||
*/
|
||||
jwtLifetimeSeconds?: number;
|
||||
|
||||
/**
|
||||
* Space-separated scopes values requested by the client.
|
||||
*/
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -258,7 +269,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,
|
||||
|
|
@ -333,6 +345,11 @@ export interface StaticPrivateKeyJwtProviderOptions {
|
|||
* Optional client name for metadata.
|
||||
*/
|
||||
clientName?: string;
|
||||
|
||||
/**
|
||||
* Space-separated scopes values requested by the client.
|
||||
*/
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -356,7 +373,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;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,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',
|
||||
|
|
@ -94,6 +123,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',
|
||||
|
|
@ -151,6 +212,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', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue