Skip to content

Commit

Permalink
Add support for Pushed Authorization Requests (#973)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Mcgrath <[email protected]>
  • Loading branch information
frederikprijck and adamjmcgrath authored Dec 7, 2023
1 parent 117214c commit 4d43370
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 2 deletions.
123 changes: 121 additions & 2 deletions src/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<JSONApiResponse<PushedAuthorizationResponse>> {
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*.
* (<strong>*Note:</string> For single-page applications and native/mobile apps, we recommend using web flows instead.)
Expand Down
11 changes: 11 additions & 0 deletions test/auth/fixtures/oauth.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
]
56 changes: 56 additions & 0 deletions test/auth/oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PasswordGrantRequest,
RefreshTokenGrantRequest,
RevokeRefreshTokenRequest,
PushedAuthorizationRequest,
} from '../../src/index.js';
import { withIdToken } from '../utils/index.js';

Expand Down Expand Up @@ -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)', () => {
Expand Down

0 comments on commit 4d43370

Please sign in to comment.