From 4d43370e53bdb77c5ae7e402a377ca92d9ef222e Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Thu, 7 Dec 2023 15:15:15 +0100 Subject: [PATCH] Add support for Pushed Authorization Requests (#973) Co-authored-by: Adam Mcgrath --- src/auth/oauth.ts | 123 +++++++++++++++++++++++++++++++++- test/auth/fixtures/oauth.json | 11 +++ test/auth/oauth.test.ts | 56 ++++++++++++++++ 3 files changed, 188 insertions(+), 2 deletions(-) diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 02c6a8462..736a0a1ac 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -13,7 +13,7 @@ export interface TokenSet { */ access_token: string; /** - * The refresh token, vavailable with the `offline_access` scope. + * The refresh token, available with the `offline_access` scope. */ refresh_token?: string; /** @@ -25,7 +25,7 @@ export interface TokenSet { */ token_type: 'Bearer'; /** - * The duration in secs that that the access token is valid. + * The duration in secs that the access token is valid. */ expires_in: number; } @@ -91,6 +91,80 @@ export interface ClientCredentialsGrantRequest extends ClientCredentials { audience: string; } +export interface PushedAuthorizationRequest extends ClientCredentials { + /** + * URI to redirect to. + */ + redirect_uri: string; + + /** + * The response_type the client expects. + */ + response_type: string; + + /** + * The response_mode to use. + */ + response_mode?: string; + + /** + * The nonce. + */ + nonce?: string; + + /** + * State value to be passed back on successful authorization. + */ + state?: string; + + /** + * Name of the connection. + */ + connection?: string; + + /** + * Scopes to request. Multiple scopes must be separated by a space character. + */ + scope?: string; + + /** + * The unique identifier of the target API you want to access. + */ + audience?: string; + + /** + * The organization to log the user in to. + */ + organization?: string; + + /** + * The id of an invitation to accept. + */ + invitation?: string; + /** + * A Base64-encoded SHA-256 hash of the {@link AuthorizationCodeGrantWithPKCERequest.code_verifier} used for the Authorization Code Flow with PKCE. + */ + code_challenge?: string; + + /** + * Allow for any custom property to be sent to Auth0 + */ + [key: string]: any; +} + +export interface PushedAuthorizationResponse { + /** + * The request URI corresponding to the authorization request posted. + * This URI is a single-use reference to the respective request data in the subsequent authorization request. + */ + request_uri: string; + + /** + * This URI is a single-use reference to the respective request data in the subsequent authorization request. + */ + expires_in: number; +} + export interface PasswordGrantRequest extends ClientCredentials { /** * The unique identifier of the target API you want to access. @@ -297,6 +371,51 @@ export class OAuth extends BaseAuthAPI { ); } + /** + * This is the OAuth 2.0 extension that allows to initiate an OAuth flow from the backchannel instead of by building a URL. + * + * + * See: https://www.rfc-editor.org/rfc/rfc9126.html + * + * @example + * ```js + * const auth0 = new AuthenticationApi({ + * domain: 'my-domain.auth0.com', + * clientId: 'myClientId', + * clientSecret: 'myClientSecret' + * }); + * + * await auth0.oauth.pushedAuthorization({ response_type: 'id_token', redirect_uri: 'http://localhost' }); + * ``` + */ + async pushedAuthorization( + bodyParameters: PushedAuthorizationRequest, + options: { initOverrides?: InitOverride } = {} + ): Promise> { + validateRequiredRequestParams(bodyParameters, ['client_id', 'response_type', 'redirect_uri']); + + const bodyParametersWithClientAuthentication = await this.addClientAuthentication( + bodyParameters + ); + + const response = await this.request( + { + path: '/oauth/par', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + client_id: this.clientId, + ...bodyParametersWithClientAuthentication, + }), + }, + options.initOverrides + ); + + return JSONApiResponse.fromResponse(response); + } + /** * This information is typically received from a highly trusted public client like a SPA*. * (*Note: For single-page applications and native/mobile apps, we recommend using web flows instead.) diff --git a/test/auth/fixtures/oauth.json b/test/auth/fixtures/oauth.json index 45d6905f4..0c8e4ac6f 100644 --- a/test/auth/fixtures/oauth.json +++ b/test/auth/fixtures/oauth.json @@ -156,5 +156,16 @@ }, "status": 200, "response": "" + }, + { + "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.com&client_secret=test-client-secret", + "status": 200, + "response": { + "request_uri": "https://www.request.uri", + "expires_in": 86400 + } } ] diff --git a/test/auth/oauth.test.ts b/test/auth/oauth.test.ts index fa18e7eac..ec61fc9f3 100644 --- a/test/auth/oauth.test.ts +++ b/test/auth/oauth.test.ts @@ -7,6 +7,7 @@ import { PasswordGrantRequest, RefreshTokenGrantRequest, RevokeRefreshTokenRequest, + PushedAuthorizationRequest, } from '../../src/index.js'; import { withIdToken } from '../utils/index.js'; @@ -273,6 +274,61 @@ describe('OAuth', () => { }); }); }); + + describe('#pushedAuthorization', () => { + it('should require a client_id', async () => { + const oauth = new OAuth(opts); + await expect(oauth.pushedAuthorization({} as PushedAuthorizationRequest)).rejects.toThrow( + 'Required parameter requestParameters.client_id was null or undefined.' + ); + }); + + it('should require a response_type', async () => { + const oauth = new OAuth(opts); + await expect( + oauth.pushedAuthorization({ client_id: 'test-client-id' } as PushedAuthorizationRequest) + ).rejects.toThrow( + 'Required parameter requestParameters.response_type was null or undefined.' + ); + }); + + it('should require a redirect_uri', async () => { + const oauth = new OAuth(opts); + await expect( + oauth.pushedAuthorization({ + client_id: 'test-client-id', + response_type: 'code', + } as PushedAuthorizationRequest) + ).rejects.toThrow('Required parameter requestParameters.redirect_uri was null or undefined.'); + }); + + it('should require a client_secret or client_assertion', async () => { + const oauth = new OAuth({ ...opts, clientSecret: undefined }); + await expect( + oauth.pushedAuthorization({ + client_id: 'test-client-id', + response_type: 'code', + redirect_uri: 'https://example.com', + } as PushedAuthorizationRequest) + ).rejects.toThrow('The client_secret or client_assertion field is required.'); + }); + + it('should return the par response', async () => { + const oauth = new OAuth(opts); + await expect( + oauth.pushedAuthorization({ + client_id: 'test-client-id', + response_type: 'code', + redirect_uri: 'https://example.com', + }) + ).resolves.toMatchObject({ + data: { + request_uri: 'https://www.request.uri', + expires_in: 86400, + }, + }); + }); + }); }); describe('OAuth (with ID Token validation)', () => {