Skip to content

Commit 50d9fa3

Browse files
authored
Fix #1430: Client Credentials providers scopes support (backported) (#1442)
1 parent aa81a66 commit 50d9fa3

File tree

2 files changed

+115
-3
lines changed

2 files changed

+115
-3
lines changed

src/client/auth-extensions.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ export interface ClientCredentialsProviderOptions {
108108
* Optional client name for metadata.
109109
*/
110110
clientName?: string;
111+
112+
/**
113+
* Space-separated scopes values requested by the client.
114+
*/
115+
scope?: string;
111116
}
112117

113118
/**
@@ -140,7 +145,8 @@ export class ClientCredentialsProvider implements OAuthClientProvider {
140145
client_name: options.clientName ?? 'client-credentials-client',
141146
redirect_uris: [],
142147
grant_types: ['client_credentials'],
143-
token_endpoint_auth_method: 'client_secret_basic'
148+
token_endpoint_auth_method: 'client_secret_basic',
149+
scope: options.scope
144150
};
145151
}
146152

@@ -216,6 +222,11 @@ export interface PrivateKeyJwtProviderOptions {
216222
* Optional JWT lifetime in seconds (default: 300).
217223
*/
218224
jwtLifetimeSeconds?: number;
225+
226+
/**
227+
* Space-separated scopes values requested by the client.
228+
*/
229+
scope?: string;
219230
}
220231

221232
/**
@@ -249,7 +260,8 @@ export class PrivateKeyJwtProvider implements OAuthClientProvider {
249260
client_name: options.clientName ?? 'private-key-jwt-client',
250261
redirect_uris: [],
251262
grant_types: ['client_credentials'],
252-
token_endpoint_auth_method: 'private_key_jwt'
263+
token_endpoint_auth_method: 'private_key_jwt',
264+
scope: options.scope
253265
};
254266
this.addClientAuthentication = createPrivateKeyJwtAuth({
255267
issuer: options.clientId,
@@ -324,6 +336,11 @@ export interface StaticPrivateKeyJwtProviderOptions {
324336
* Optional client name for metadata.
325337
*/
326338
clientName?: string;
339+
340+
/**
341+
* Space-separated scopes values requested by the client.
342+
*/
343+
scope?: string;
327344
}
328345

329346
/**
@@ -347,7 +364,8 @@ export class StaticPrivateKeyJwtProvider implements OAuthClientProvider {
347364
client_name: options.clientName ?? 'static-private-key-jwt-client',
348365
redirect_uris: [],
349366
grant_types: ['client_credentials'],
350-
token_endpoint_auth_method: 'private_key_jwt'
367+
token_endpoint_auth_method: 'private_key_jwt',
368+
scope: options.scope
351369
};
352370

353371
const assertion = options.jwtBearerAssertion;

test/client/auth-extensions.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,35 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
4949
expect(tokens?.access_token).toBe('test-access-token');
5050
});
5151

52+
it('sends scope in token request when ClientCredentialsProvider is configured with scope', async () => {
53+
const provider = new ClientCredentialsProvider({
54+
clientId: 'my-client',
55+
clientSecret: 'my-secret',
56+
clientName: 'test-client',
57+
scope: 'read write'
58+
});
59+
60+
expect(provider.clientMetadata.scope).toBe('read write');
61+
62+
const fetchMock = createMockOAuthFetch({
63+
resourceServerUrl: RESOURCE_SERVER_URL,
64+
authServerUrl: AUTH_SERVER_URL,
65+
onTokenRequest: async (_url, init) => {
66+
const params = init?.body as URLSearchParams;
67+
expect(params).toBeInstanceOf(URLSearchParams);
68+
expect(params.get('grant_type')).toBe('client_credentials');
69+
expect(params.get('scope')).toBe('read write');
70+
}
71+
});
72+
73+
const result = await auth(provider, {
74+
serverUrl: RESOURCE_SERVER_URL,
75+
fetchFn: fetchMock
76+
});
77+
78+
expect(result).toBe('AUTHORIZED');
79+
});
80+
5281
it('authenticates using PrivateKeyJwtProvider with private_key_jwt', async () => {
5382
const provider = new PrivateKeyJwtProvider({
5483
clientId: 'client-id',
@@ -92,6 +121,38 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
92121
expect(assertionFromRequest).toBeTruthy();
93122
});
94123

124+
it('sends scope in token request when PrivateKeyJwtProvider is configured with scope', async () => {
125+
const provider = new PrivateKeyJwtProvider({
126+
clientId: 'client-id',
127+
privateKey: 'a-string-secret-at-least-256-bits-long',
128+
algorithm: 'HS256',
129+
clientName: 'private-key-jwt-client',
130+
scope: 'openid profile'
131+
});
132+
133+
expect(provider.clientMetadata.scope).toBe('openid profile');
134+
135+
const fetchMock = createMockOAuthFetch({
136+
resourceServerUrl: RESOURCE_SERVER_URL,
137+
authServerUrl: AUTH_SERVER_URL,
138+
onTokenRequest: async (_url, init) => {
139+
const params = init?.body as URLSearchParams;
140+
expect(params).toBeInstanceOf(URLSearchParams);
141+
expect(params.get('grant_type')).toBe('client_credentials');
142+
expect(params.get('scope')).toBe('openid profile');
143+
expect(params.get('client_assertion')).toBeTruthy();
144+
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
145+
}
146+
});
147+
148+
const result = await auth(provider, {
149+
serverUrl: RESOURCE_SERVER_URL,
150+
fetchFn: fetchMock
151+
});
152+
153+
expect(result).toBe('AUTHORIZED');
154+
});
155+
95156
it('fails when PrivateKeyJwtProvider is configured with an unsupported algorithm', async () => {
96157
const provider = new PrivateKeyJwtProvider({
97158
clientId: 'client-id',
@@ -149,6 +210,39 @@ describe('auth-extensions providers (end-to-end with auth())', () => {
149210
expect(tokens).toBeTruthy();
150211
expect(tokens?.access_token).toBe('test-access-token');
151212
});
213+
214+
it('sends scope in token request when StaticPrivateKeyJwtProvider is configured with scope', async () => {
215+
const staticAssertion = 'header.payload.signature';
216+
217+
const provider = new StaticPrivateKeyJwtProvider({
218+
clientId: 'static-client',
219+
jwtBearerAssertion: staticAssertion,
220+
clientName: 'static-private-key-jwt-client',
221+
scope: 'api:read api:write'
222+
});
223+
224+
expect(provider.clientMetadata.scope).toBe('api:read api:write');
225+
226+
const fetchMock = createMockOAuthFetch({
227+
resourceServerUrl: RESOURCE_SERVER_URL,
228+
authServerUrl: AUTH_SERVER_URL,
229+
onTokenRequest: async (_url, init) => {
230+
const params = init?.body as URLSearchParams;
231+
expect(params).toBeInstanceOf(URLSearchParams);
232+
expect(params.get('grant_type')).toBe('client_credentials');
233+
expect(params.get('scope')).toBe('api:read api:write');
234+
expect(params.get('client_assertion')).toBe(staticAssertion);
235+
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
236+
}
237+
});
238+
239+
const result = await auth(provider, {
240+
serverUrl: RESOURCE_SERVER_URL,
241+
fetchFn: fetchMock
242+
});
243+
244+
expect(result).toBe('AUTHORIZED');
245+
});
152246
});
153247

154248
describe('createPrivateKeyJwtAuth', () => {

0 commit comments

Comments
 (0)