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
24 changes: 24 additions & 0 deletions packages/client/src/client/authExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export interface ClientCredentialsProviderOptions {
* Optional client name for metadata.
*/
clientName?: string;

/**
* Optional scope to request during token exchange.
*/
scope?: string;
}

/**
Expand Down Expand Up @@ -148,6 +153,9 @@ export class ClientCredentialsProvider implements OAuthClientProvider {
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'client_secret_basic'
};
if (options.scope !== undefined) {
this._clientMetadata.scope = options.scope;
}
}

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

/**
* Optional scope to request during token exchange.
*/
scope?: string;
}

/**
Expand Down Expand Up @@ -260,6 +273,9 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider {
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'private_key_jwt'
};
if (options.scope !== undefined) {
this._clientMetadata.scope = options.scope;
}
this.addClientAuthentication = createPrivateKeyJwtAuth({
issuer: options.clientId,
subject: options.clientId,
Expand Down Expand Up @@ -333,6 +349,11 @@ export interface StaticPrivateKeyJwtProviderOptions {
* Optional client name for metadata.
*/
clientName?: string;

/**
* Optional scope to request during token exchange.
*/
scope?: string;
}

/**
Expand All @@ -358,6 +379,9 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider {
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'private_key_jwt'
};
if (options.scope !== undefined) {
this._clientMetadata.scope = options.scope;
}

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

it('uses configured scope for ClientCredentialsProvider token requests', async () => {
const provider = new ClientCredentialsProvider({
clientId: 'my-client',
clientSecret: 'my-secret',
scope: 'mcp:read mcp:write'
});

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

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

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

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

it('uses configured scope for PrivateKeyJwtProvider token requests', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
algorithm: 'HS256',
scope: 'mcp:read mcp:write'
});

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

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

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

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

it('uses configured scope for StaticPrivateKeyJwtProvider token requests', async () => {
const provider = new StaticPrivateKeyJwtProvider({
clientId: 'static-client',
jwtBearerAssertion: 'header.payload.signature',
scope: 'mcp:read mcp:write'
});

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

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

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

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

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