Skip to content
Merged
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
108 changes: 108 additions & 0 deletions __tests__/Auth0Client/exchangeToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,5 +327,113 @@ describe('Auth0Client', () => {
'openid profile read:sensitive'
);
});

it('passes organization parameter to _requestToken when provided', async () => {
const auth0 = await localSetup({
clientId: 'test-client-id',
domain: 'test.auth0.com',
authorizationParams: {
audience: 'https://default-api.com',
scope: 'openid profile'
}
});

let capturedRequestOptions: any;
auth0['_requestToken'] = async function (requestOptions: any) {
capturedRequestOptions = requestOptions;
return {
decodedToken: {
encoded: {
header: 'fake_header',
payload: 'fake_payload',
signature: 'fake_signature'
},
header: {},
claims: {
__raw: 'fake_raw',
org_id: 'org_12345' // Organization ID in token claims
},
user: {}
},
id_token: 'fake_id_token',
access_token: 'fake_access_token',
token_type: 'Bearer',
expires_in: 3600,
scope: requestOptions.scope
};
};

const cteOptions: CustomTokenExchangeOptions = {
subject_token: 'external_token_value',
subject_token_type: 'urn:acme:legacy-system-token',
scope: 'openid profile email',
audience: 'https://api.custom.com',
organization: 'org_12345'
};

await auth0.exchangeToken(cteOptions);

expect(capturedRequestOptions).toEqual({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: 'external_token_value',
subject_token_type: 'urn:acme:legacy-system-token',
scope: 'openid profile email',
audience: 'https://api.custom.com',
organization: 'org_12345'
});
});

it('does not pass organization parameter when not provided', async () => {
const auth0 = await localSetup({
clientId: 'test-client-id',
domain: 'test.auth0.com',
authorizationParams: {
audience: 'https://default-api.com',
scope: 'openid profile'
}
});

let capturedRequestOptions: any;
auth0['_requestToken'] = async function (requestOptions: any) {
capturedRequestOptions = requestOptions;
return {
decodedToken: {
encoded: {
header: 'fake_header',
payload: 'fake_payload',
signature: 'fake_signature'
},
header: {},
claims: { __raw: 'fake_raw' },
user: {}
},
id_token: 'fake_id_token',
access_token: 'fake_access_token',
token_type: 'Bearer',
expires_in: 3600,
scope: requestOptions.scope
};
};

const cteOptions: CustomTokenExchangeOptions = {
subject_token: 'external_token_value',
subject_token_type: 'urn:acme:legacy-system-token',
scope: 'openid profile email',
audience: 'https://api.custom.com'
// organization is not provided
};

await auth0.exchangeToken(cteOptions);

// organization should not be present in the request options
expect(capturedRequestOptions).toEqual({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: 'external_token_value',
subject_token_type: 'urn:acme:legacy-system-token',
scope: 'openid profile email',
audience: 'https://api.custom.com'
});
expect(capturedRequestOptions.organization).toBeUndefined();
});
});
});
10 changes: 8 additions & 2 deletions src/Auth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,8 @@ export class Auth0Client {
* - `scope`: A unique set of scopes, generated by merging the scopes supplied in the options
* with the SDK’s default scopes.
* - `audience`: The target audience from the options, with fallback to the SDK's authorization configuration.
* - `organization`: Optional organization ID or name for authenticating the user in an organization context.
* When provided, the organization ID will be present in the access token payload.
*
* **Example Usage:**
*
Expand All @@ -1430,13 +1432,15 @@ export class Auth0Client {
* const options: CustomTokenExchangeOptions = {
* subject_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...',
* subject_token_type: 'urn:acme:legacy-system-token',
* scope: "openid profile"
* scope: "openid profile",
* organization: "org_12345"
* };
*
* // Exchange the external token for Auth0 tokens
* try {
* const tokenResponse = await instance.exchangeToken(options);
* // Use tokenResponse.access_token, tokenResponse.id_token, etc.
* // The organization ID will be present in the access token payload
* } catch (error) {
* // Handle token exchange error
* }
Expand All @@ -1454,7 +1458,8 @@ export class Auth0Client {
options.scope,
options.audience || this.options.authorizationParams.audience
),
audience: options.audience || this.options.authorizationParams.audience
audience: options.audience || this.options.authorizationParams.audience,
...(options.organization && { organization: options.organization })
});
}

Expand Down Expand Up @@ -1638,6 +1643,7 @@ interface TokenExchangeRequestOptions extends BaseRequestTokenOptions {
subject_token_type: string;
actor_token?: string;
actor_token_type?: string;
organization?: string;
}

interface RequestTokenAdditionalParameters {
Expand Down
7 changes: 7 additions & 0 deletions src/TokenExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export type CustomTokenExchangeOptions = {
*/
scope?: string;

/**
* ID or name of the organization to use when authenticating a user.
* When provided, the user will be authenticated using the organization context.
* The organization ID will be present in the access token payload.
*/
organization?: string;

/**
* Additional custom parameters for Auth0 Action processing
*
Expand Down