Skip to content

Commit 8656744

Browse files
feat: add organization parameter support to token exchange (#1477)
## Description Adds support for the `organization` parameter in the token exchange flow (`exchangeToken` method) to enable organization-specific authentication. ## Changes - Added `organization` as an optional parameter to `CustomTokenExchangeOptions` interface - Updated `exchangeToken` method to conditionally pass organization to the `/oauth/token` endpoint - Updated JSDoc documentation with organization parameter details and usage example - Added test coverage for organization parameter (both provided and not provided scenarios) ## API Changes The `/oauth/token` endpoint now accepts an optional `organization` parameter for the token exchange grant type. When provided, the organization ID will be present in the access token payload. ## Testing - ✅ Added test: passes organization parameter to _requestToken when provided - ✅ Added test: does not pass organization parameter when not provided - ✅ All existing tests pass ## Example Usage ```typescript const tokenResponse = await auth0Client.exchangeToken({ subject_token: 'external_token', subject_token_type: 'urn:acme:legacy-system-token', scope: 'openid profile', organization: 'org_12345' }); // Organization ID will be in access token payload ```
1 parent 9103021 commit 8656744

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

__tests__/Auth0Client/exchangeToken.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,5 +327,113 @@ describe('Auth0Client', () => {
327327
'openid profile read:sensitive'
328328
);
329329
});
330+
331+
it('passes organization parameter to _requestToken when provided', async () => {
332+
const auth0 = await localSetup({
333+
clientId: 'test-client-id',
334+
domain: 'test.auth0.com',
335+
authorizationParams: {
336+
audience: 'https://default-api.com',
337+
scope: 'openid profile'
338+
}
339+
});
340+
341+
let capturedRequestOptions: any;
342+
auth0['_requestToken'] = async function (requestOptions: any) {
343+
capturedRequestOptions = requestOptions;
344+
return {
345+
decodedToken: {
346+
encoded: {
347+
header: 'fake_header',
348+
payload: 'fake_payload',
349+
signature: 'fake_signature'
350+
},
351+
header: {},
352+
claims: {
353+
__raw: 'fake_raw',
354+
org_id: 'org_12345' // Organization ID in token claims
355+
},
356+
user: {}
357+
},
358+
id_token: 'fake_id_token',
359+
access_token: 'fake_access_token',
360+
token_type: 'Bearer',
361+
expires_in: 3600,
362+
scope: requestOptions.scope
363+
};
364+
};
365+
366+
const cteOptions: CustomTokenExchangeOptions = {
367+
subject_token: 'external_token_value',
368+
subject_token_type: 'urn:acme:legacy-system-token',
369+
scope: 'openid profile email',
370+
audience: 'https://api.custom.com',
371+
organization: 'org_12345'
372+
};
373+
374+
await auth0.exchangeToken(cteOptions);
375+
376+
expect(capturedRequestOptions).toEqual({
377+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
378+
subject_token: 'external_token_value',
379+
subject_token_type: 'urn:acme:legacy-system-token',
380+
scope: 'openid profile email',
381+
audience: 'https://api.custom.com',
382+
organization: 'org_12345'
383+
});
384+
});
385+
386+
it('does not pass organization parameter when not provided', async () => {
387+
const auth0 = await localSetup({
388+
clientId: 'test-client-id',
389+
domain: 'test.auth0.com',
390+
authorizationParams: {
391+
audience: 'https://default-api.com',
392+
scope: 'openid profile'
393+
}
394+
});
395+
396+
let capturedRequestOptions: any;
397+
auth0['_requestToken'] = async function (requestOptions: any) {
398+
capturedRequestOptions = requestOptions;
399+
return {
400+
decodedToken: {
401+
encoded: {
402+
header: 'fake_header',
403+
payload: 'fake_payload',
404+
signature: 'fake_signature'
405+
},
406+
header: {},
407+
claims: { __raw: 'fake_raw' },
408+
user: {}
409+
},
410+
id_token: 'fake_id_token',
411+
access_token: 'fake_access_token',
412+
token_type: 'Bearer',
413+
expires_in: 3600,
414+
scope: requestOptions.scope
415+
};
416+
};
417+
418+
const cteOptions: CustomTokenExchangeOptions = {
419+
subject_token: 'external_token_value',
420+
subject_token_type: 'urn:acme:legacy-system-token',
421+
scope: 'openid profile email',
422+
audience: 'https://api.custom.com'
423+
// organization is not provided
424+
};
425+
426+
await auth0.exchangeToken(cteOptions);
427+
428+
// organization should not be present in the request options
429+
expect(capturedRequestOptions).toEqual({
430+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
431+
subject_token: 'external_token_value',
432+
subject_token_type: 'urn:acme:legacy-system-token',
433+
scope: 'openid profile email',
434+
audience: 'https://api.custom.com'
435+
});
436+
expect(capturedRequestOptions.organization).toBeUndefined();
437+
});
330438
});
331439
});

src/Auth0Client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,8 @@ export class Auth0Client {
14221422
* - `scope`: A unique set of scopes, generated by merging the scopes supplied in the options
14231423
* with the SDK’s default scopes.
14241424
* - `audience`: The target audience from the options, with fallback to the SDK's authorization configuration.
1425+
* - `organization`: Optional organization ID or name for authenticating the user in an organization context.
1426+
* When provided, the organization ID will be present in the access token payload.
14251427
*
14261428
* **Example Usage:**
14271429
*
@@ -1430,13 +1432,15 @@ export class Auth0Client {
14301432
* const options: CustomTokenExchangeOptions = {
14311433
* subject_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...',
14321434
* subject_token_type: 'urn:acme:legacy-system-token',
1433-
* scope: "openid profile"
1435+
* scope: "openid profile",
1436+
* organization: "org_12345"
14341437
* };
14351438
*
14361439
* // Exchange the external token for Auth0 tokens
14371440
* try {
14381441
* const tokenResponse = await instance.exchangeToken(options);
14391442
* // Use tokenResponse.access_token, tokenResponse.id_token, etc.
1443+
* // The organization ID will be present in the access token payload
14401444
* } catch (error) {
14411445
* // Handle token exchange error
14421446
* }
@@ -1454,7 +1458,8 @@ export class Auth0Client {
14541458
options.scope,
14551459
options.audience || this.options.authorizationParams.audience
14561460
),
1457-
audience: options.audience || this.options.authorizationParams.audience
1461+
audience: options.audience || this.options.authorizationParams.audience,
1462+
...(options.organization && { organization: options.organization })
14581463
});
14591464
}
14601465

@@ -1638,6 +1643,7 @@ interface TokenExchangeRequestOptions extends BaseRequestTokenOptions {
16381643
subject_token_type: string;
16391644
actor_token?: string;
16401645
actor_token_type?: string;
1646+
organization?: string;
16411647
}
16421648

16431649
interface RequestTokenAdditionalParameters {

src/TokenExchange.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ export type CustomTokenExchangeOptions = {
5757
*/
5858
scope?: string;
5959

60+
/**
61+
* ID or name of the organization to use when authenticating a user.
62+
* When provided, the user will be authenticated using the organization context.
63+
* The organization ID will be present in the access token payload.
64+
*/
65+
organization?: string;
66+
6067
/**
6168
* Additional custom parameters for Auth0 Action processing
6269
*

0 commit comments

Comments
 (0)