diff --git a/src/auth/backchannel.ts b/src/auth/backchannel.ts index 707a6bd00..0548023a1 100644 --- a/src/auth/backchannel.ts +++ b/src/auth/backchannel.ts @@ -73,7 +73,9 @@ const getLoginHint = (userId: string, domain: string): string => { /** * Options for the authorize request. */ -export type AuthorizeOptions = { +export interface AuthorizeOptions< + TAuthorizationDetails extends AuthorizationDetails = AuthorizationDetails +> { /** * A human-readable string intended to be displayed on both the device calling /bc-authorize and the user’s authentication device. */ @@ -102,18 +104,28 @@ export type AuthorizeOptions = { * Optional authorization details to use Rich Authorization Requests (RAR). * @see https://auth0.com/docs/get-started/apis/configure-rich-authorization-requests */ - authorization_details?: string; -} & Record; + authorization_details?: string | TAuthorizationDetails[]; + + [key: string]: unknown; +} type AuthorizeRequest = Omit & AuthorizeCredentialsPartial & { login_hint: string; - }; + authorization_details?: string; + } & Record; + +interface AuthorizationDetails { + readonly type: string; + readonly [parameter: string]: unknown; +} /** * The response from the token endpoint. */ -export type TokenResponse = { +export interface TokenResponse< + TAuthorizationDetails extends AuthorizationDetails = AuthorizationDetails +> { /** * The access token. */ @@ -142,8 +154,8 @@ export type TokenResponse = { * Optional authorization details when using Rich Authorization Requests (RAR). * @see https://auth0.com/docs/get-started/apis/configure-rich-authorization-requests */ - authorization_details?: string; -}; + authorization_details?: TAuthorizationDetails[]; +} /** * Options for the token request. @@ -185,8 +197,18 @@ export class Backchannel extends BaseAuthAPI implements IBackchannel { * @throws {Error} - If the request fails. */ async authorize({ userId, ...options }: AuthorizeOptions): Promise { + const { authorization_details, ...authorizeOptions } = options; + + if (authorization_details) { + // Convert to string if not already + authorizeOptions.authorization_details = + typeof authorization_details !== 'string' + ? JSON.stringify(authorization_details) + : authorization_details; + } + const body: AuthorizeRequest = { - ...options, + ...authorizeOptions, login_hint: getLoginHint(userId, this.domain), client_id: this.clientId, }; @@ -239,7 +261,9 @@ export class Backchannel extends BaseAuthAPI implements IBackchannel { * } * ``` */ - async backchannelGrant({ auth_req_id }: TokenOptions): Promise { + async backchannelGrant< + TAuthorizationDetails extends AuthorizationDetails = AuthorizationDetails + >({ auth_req_id }: TokenOptions): Promise> { const body: TokenRequestBody = { client_id: this.clientId, auth_req_id, @@ -258,7 +282,8 @@ export class Backchannel extends BaseAuthAPI implements IBackchannel { {} ); - const r: JSONApiResponse = await JSONApiResponse.fromResponse(response); + const r: JSONApiResponse> = + await JSONApiResponse.fromResponse(response); return r.data; } } diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 71de7584a..9c53d633c 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -8,7 +8,14 @@ import { BaseAuthAPI, AuthenticationClientOptions, grant } from './base-auth-api import { IDTokenValidateOptions, IDTokenValidator } from './id-token-validator.js'; import { mtlsPrefix } from '../utils.js'; -export interface TokenSet { +interface AuthorizationDetails { + readonly type: string; + readonly [parameter: string]: unknown; +} + +export interface TokenSet< + TAuthorizationDetails extends AuthorizationDetails = AuthorizationDetails +> { /** * The access token. */ @@ -29,6 +36,11 @@ export interface TokenSet { * The duration in secs that the access token is valid. */ expires_in: number; + /** + * The authorization details when using Rich Authorization Requests (RAR). + * @see https://auth0.com/docs/get-started/apis/configure-rich-authorization-requests + */ + authorization_details?: TAuthorizationDetails[]; } export interface GrantOptions { @@ -99,7 +111,9 @@ export interface ClientCredentialsGrantRequest extends ClientCredentials { organization?: string; } -export interface PushedAuthorizationRequest extends ClientCredentials { +export interface PushedAuthorizationRequest< + TAuthorizationDetails extends AuthorizationDetails = AuthorizationDetails +> extends ClientCredentials { /** * URI to redirect to. */ @@ -162,7 +176,7 @@ export interface PushedAuthorizationRequest extends ClientCredentials { /** * A JSON stringified array of objects. It can carry fine-grained authorization data in OAuth messages as part of Rich Authorization Requests (RAR) {@link https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow/authorization-code-flow-with-rar | Reference} */ - authorization_details?: string; + authorization_details?: string | TAuthorizationDetails[]; /** * Allow for any custom property to be sent to Auth0 @@ -449,6 +463,15 @@ export class OAuth extends BaseAuthAPI { options: { initOverrides?: InitOverride } = {} ): Promise> { validateRequiredRequestParams(bodyParameters, ['client_id', 'response_type', 'redirect_uri']); + const { authorization_details } = bodyParameters; + + if (authorization_details) { + // Convert to string if not already + bodyParameters.authorization_details = + typeof authorization_details !== 'string' + ? JSON.stringify(authorization_details) + : authorization_details; + } const bodyParametersWithClientAuthentication = await this.addClientAuthentication( bodyParameters diff --git a/test/auth/backchannel.test.ts b/test/auth/backchannel.test.ts index 1f998ca44..1d6e4c9e6 100644 --- a/test/auth/backchannel.test.ts +++ b/test/auth/backchannel.test.ts @@ -238,7 +238,7 @@ describe('Backchannel', () => { }); it('should return token response, including authorization_details when available', async () => { - const authorization_details = JSON.stringify([{ type: 'test-type' }]); + const authorization_details = [{ type: 'test-type' }]; nock(`https://${opts.domain}`).post('/oauth/token').reply(200, { access_token: 'test-access-token', id_token: 'test-id-token', diff --git a/test/auth/fixtures/oauth.json b/test/auth/fixtures/oauth.json index 8c0fca5fa..e4affc656 100644 --- a/test/auth/fixtures/oauth.json +++ b/test/auth/fixtures/oauth.json @@ -168,6 +168,17 @@ "expires_in": 86400 } }, + { + "scope": "https://test-domain.auth0.com", + "method": "POST", + "path": "/oauth/par", + "body": "client_id=test-client-id&response_type=code&redirect_uri=https%3A%2F%2Fexample-as-string.com&authorization_details=%5B%7B%22type%22%3A%22payment_initiation%22%2C%22actions%22%3A%5B%22write%22%5D%7D%5D&client_secret=test-client-secret", + "status": 200, + "response": { + "request_uri": "https://www.request.uri", + "expires_in": 86400 + } + }, { "scope": "https://test-domain.auth0.com", "method": "POST", diff --git a/test/auth/oauth.test.ts b/test/auth/oauth.test.ts index 9aaec7dbc..820b0b4f1 100644 --- a/test/auth/oauth.test.ts +++ b/test/auth/oauth.test.ts @@ -332,13 +332,13 @@ describe('OAuth', () => { }); }); - it('should send authorization_details when provided', async () => { + it('should send authorization_details when provided as string', async () => { const oauth = new OAuth(opts); await expect( oauth.pushedAuthorization({ client_id: 'test-client-id', response_type: 'code', - redirect_uri: 'https://example.com', + redirect_uri: 'https://example-as-string.com', authorization_details: JSON.stringify([ { type: 'payment_initiation', actions: ['write'] }, ]), @@ -351,6 +351,23 @@ describe('OAuth', () => { }); }); + it('should send authorization_details when provided as array', async () => { + const oauth = new OAuth(opts); + await expect( + oauth.pushedAuthorization({ + client_id: 'test-client-id', + response_type: 'code', + redirect_uri: 'https://example.com', + authorization_details: [{ type: 'payment_initiation', actions: ['write'] }], + }) + ).resolves.toMatchObject({ + data: { + request_uri: 'https://www.request.uri', + expires_in: 86400, + }, + }); + }); + it('should send request param when provided', async () => { const oauth = new OAuth(opts); await expect(