diff --git a/src/auth/backchannel.ts b/src/auth/backchannel.ts index 2cd8425c60..ceddfcb45f 100644 --- a/src/auth/backchannel.ts +++ b/src/auth/backchannel.ts @@ -8,6 +8,7 @@ import { JSONApiResponse } from "../lib/models.js"; import { BaseAuthAPI } from "./base-auth-api.js"; +import { AuthorizationDetails } from "./oauth.js"; /** * The response from the authorize endpoint. @@ -73,7 +74,7 @@ const getLoginHint = (userId: string, domain: string): string => { /** * Options for the authorize request. */ -export type AuthorizeOptions = { +export interface AuthorizeOptions { /** * A human-readable string intended to be displayed on both the device calling /bc-authorize and the user’s authentication device. */ @@ -107,23 +108,21 @@ 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; - }; - -export interface AuthorizationDetails { - readonly type: string; - readonly [parameter: string]: unknown; -} + authorization_details?: string; + } & Record; /** * The response from the token endpoint. */ -export type TokenResponse = { +export interface TokenResponse { /** * The access token. */ @@ -152,8 +151,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?: AuthorizationDetails[]; -}; + authorization_details?: TAuthorizationDetails[]; +} /** * Options for the token request. @@ -174,8 +173,12 @@ type TokenRequestBody = AuthorizeCredentialsPartial & { * Interface for the backchannel authentication. */ export interface IBackchannel { - authorize: (options: AuthorizeOptions) => Promise; - backchannelGrant: (options: TokenOptions) => Promise; + authorize: ( + options: AuthorizeOptions, + ) => Promise; + backchannelGrant: ( + options: TokenOptions, + ) => Promise>; } const CIBA_GRANT_TYPE = "urn:openid:params:grant-type:ciba"; @@ -194,9 +197,21 @@ export class Backchannel extends BaseAuthAPI implements IBackchannel { * * @throws {Error} - If the request fails. */ - async authorize({ userId, ...options }: AuthorizeOptions): Promise { + async authorize({ + userId, + ...options + }: AuthorizeOptions): Promise { + const { authorization_details, ...authorizeOptions } = options; + + if (authorization_details) { + authorizeOptions.authorization_details = + typeof authorization_details === "string" + ? authorization_details + : JSON.stringify(authorization_details); + } + const body: AuthorizeRequest = { - ...options, + ...authorizeOptions, login_hint: getLoginHint(userId, this.domain), client_id: this.clientId, }; @@ -255,7 +270,9 @@ export class Backchannel extends BaseAuthAPI implements IBackchannel { * } * ``` */ - async backchannelGrant({ auth_req_id }: TokenOptions): Promise { + async backchannelGrant({ + auth_req_id, + }: TokenOptions): Promise> { const body: TokenRequestBody = { client_id: this.clientId, auth_req_id, @@ -274,7 +291,7 @@ 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/base-auth-api.ts b/src/auth/base-auth-api.ts index 5300e861b6..a143b0585a 100644 --- a/src/auth/base-auth-api.ts +++ b/src/auth/base-auth-api.ts @@ -2,7 +2,7 @@ import { ResponseError } from "../lib/errors.js"; import { BaseAPI, ClientOptions, InitOverrideFunction, JSONApiResponse, RequestOpts } from "../lib/runtime.js"; import { AddClientAuthenticationPayload, addClientAuthentication } from "./client-authentication.js"; import { IDTokenValidator } from "./id-token-validator.js"; -import { GrantOptions, TokenSet } from "./oauth.js"; +import { AuthorizationDetails, GrantOptions, TokenSet } from "./oauth.js"; import { Auth0ClientTelemetry } from "../lib/middleware/auth0-client-telemetry.js"; export interface AuthenticationClientOptions extends ClientOptions { @@ -114,14 +114,14 @@ export class BaseAuthAPI extends BaseAPI { * @private * Perform an OAuth 2.0 grant. */ -export async function grant( +export async function grant( grantType: string, bodyParameters: Record, { idTokenValidateOptions, initOverrides }: GrantOptions = {}, clientId: string, idTokenValidator: IDTokenValidator, request: (context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) => Promise, -): Promise> { +): Promise>> { const response = await request( { path: "/oauth/token", @@ -138,7 +138,7 @@ export async function grant( initOverrides, ); - const res: JSONApiResponse = await JSONApiResponse.fromResponse(response); + const res: JSONApiResponse> = await JSONApiResponse.fromResponse(response); if (res.data.id_token) { await idTokenValidator.validate(res.data.id_token, idTokenValidateOptions); } diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 62e3806e00..9608bcf12a 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -3,7 +3,12 @@ import { BaseAuthAPI, AuthenticationClientOptions, grant } from "./base-auth-api import { IDTokenValidateOptions, IDTokenValidator } from "./id-token-validator.js"; import { mtlsPrefix } from "../utils.js"; -export interface TokenSet { +export interface AuthorizationDetails { + readonly type: string; + readonly [parameter: string]: unknown; +} + +export interface TokenSet { /** * The access token. */ @@ -24,6 +29,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 { @@ -94,7 +104,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. */ @@ -157,7 +169,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 @@ -359,13 +371,13 @@ export class OAuth extends BaseAuthAPI { * await auth0.oauth.authorizationCodeGrant({ code: 'mycode' }); * ``` */ - async authorizationCodeGrant( + async authorizationCodeGrant( bodyParameters: AuthorizationCodeGrantRequest, options: AuthorizationCodeGrantOptions = {}, - ): Promise> { + ): Promise>> { validateRequiredRequestParams(bodyParameters, ["code"]); - return grant( + return grant( "authorization_code", await this.addClientAuthentication(bodyParameters), options, @@ -396,13 +408,13 @@ export class OAuth extends BaseAuthAPI { * }); * ``` */ - async authorizationCodeGrantWithPKCE( + async authorizationCodeGrantWithPKCE( bodyParameters: AuthorizationCodeGrantWithPKCERequest, options: AuthorizationCodeGrantOptions = {}, - ): Promise> { + ): Promise>> { validateRequiredRequestParams(bodyParameters, ["code", "code_verifier"]); - return grant( + return grant( "authorization_code", await this.addClientAuthentication(bodyParameters), options, @@ -431,13 +443,13 @@ export class OAuth extends BaseAuthAPI { * await auth0.oauth.clientCredentialsGrant({ audience: 'myaudience' }); * ``` */ - async clientCredentialsGrant( + async clientCredentialsGrant( bodyParameters: ClientCredentialsGrantRequest, options: { initOverrides?: InitOverride } = {}, - ): Promise> { + ): Promise>> { validateRequiredRequestParams(bodyParameters, ["audience"]); - return grant( + return grant( "client_credentials", await this.addClientAuthentication(bodyParameters), options, @@ -469,8 +481,16 @@ export class OAuth extends BaseAuthAPI { options: { initOverrides?: InitOverride } = {}, ): Promise> { validateRequiredRequestParams(bodyParameters, ["client_id", "response_type", "redirect_uri"]); + const { authorization_details, ...params } = bodyParameters; + + if (authorization_details) { + params.authorization_details = + typeof authorization_details === "string" + ? authorization_details + : JSON.stringify(authorization_details); + } - const bodyParametersWithClientAuthentication = await this.addClientAuthentication(bodyParameters); + const bodyParametersWithClientAuthentication = await this.addClientAuthentication(params); const response = await this.request( { @@ -518,13 +538,13 @@ export class OAuth extends BaseAuthAPI { * See https://auth0.com/docs/get-started/authentication-and-authorization-flow/avoid-common-issues-with-resource-owner-password-flow-and-attack-protection * */ - async passwordGrant( + async passwordGrant( bodyParameters: PasswordGrantRequest, options: GrantOptions = {}, - ): Promise> { + ): Promise>> { validateRequiredRequestParams(bodyParameters, ["username", "password"]); - return grant( + return grant( bodyParameters.realm ? "http://auth0.com/oauth/grant-type/password-realm" : "password", await this.addClientAuthentication(bodyParameters), options, @@ -550,13 +570,13 @@ export class OAuth extends BaseAuthAPI { * await auth0.oauth.refreshTokenGrant({ refresh_token: 'myrefreshtoken' }) * ``` */ - async refreshTokenGrant( + async refreshTokenGrant( bodyParameters: RefreshTokenGrantRequest, options: GrantOptions = {}, - ): Promise> { + ): Promise>> { validateRequiredRequestParams(bodyParameters, ["refresh_token"]); - return grant( + return grant( "refresh_token", await this.addClientAuthentication(bodyParameters), options, diff --git a/tests/auth/backchannel.test.ts b/tests/auth/backchannel.test.ts index fd57e0c4b1..74019e11e0 100644 --- a/tests/auth/backchannel.test.ts +++ b/tests/auth/backchannel.test.ts @@ -145,7 +145,7 @@ describe("Backchannel", () => { expect(receivedRequestExpiry).toBe(999); }); - it("should pass authorization_details to /bc-authorize", async () => { + it("should pass authorization_details to /bc-authorize when provided as string", async () => { let receivedAuthorizationDetails: { type: string }[] = []; nock(`https://${opts.domain}`) .post("/bc-authorize") @@ -170,6 +170,29 @@ describe("Backchannel", () => { expect(receivedAuthorizationDetails[0].type).toBe("test-type"); }); + it("should serialize authorization_details to a JSON string when provided as array", async () => { + let rawAuthorizationDetails = ""; + nock(`https://${opts.domain}`) + .post("/bc-authorize") + .reply(201, (uri, requestBody, cb) => { + rawAuthorizationDetails = querystring.parse(requestBody as any)["authorization_details"] as string; + cb(null, { + auth_req_id: "test-auth-req-id", + expires_in: 300, + interval: 5, + }); + }); + + await backchannel.authorize({ + userId: "auth0|test-user-id", + binding_message: "Test binding message", + scope: "openid", + authorization_details: [{ type: "test-type", actions: ["read"] }], + }); + + expect(rawAuthorizationDetails).toBe(JSON.stringify([{ type: "test-type", actions: ["read"] }])); + }); + it("should pass custom parameters to /bc-authorize", async () => { let receivedCustomParam = ""; nock(`https://${opts.domain}`) @@ -292,7 +315,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/tests/auth/fixtures/oauth.json b/tests/auth/fixtures/oauth.json index 68db2792be..f0fbe625f1 100644 --- a/tests/auth/fixtures/oauth.json +++ b/tests/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/tests/auth/oauth.test.ts b/tests/auth/oauth.test.ts index c4e522615b..99366bf649 100644 --- a/tests/auth/oauth.test.ts +++ b/tests/auth/oauth.test.ts @@ -323,13 +323,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"] }]), }), ).resolves.toMatchObject({ @@ -340,6 +340,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(