Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Pushed Authorization Requests #973

Merged
merged 3 commits into from
Dec 7, 2023
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
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;
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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