Skip to content

Commit

Permalink
Merge pull request #148 from badgateway/response-mode-fragment
Browse files Browse the repository at this point in the history
Support for response_mode=fragment
  • Loading branch information
evert authored Jul 27, 2024
2 parents a9b88ff + dfb1c38 commit 39a7671
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x]
node-version: [14.x, 16.x, 18.x, 20.x, 22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ since Node 18 (but it works with Polyfills on Node 14 and 16).
* OAuth2 Token Introspection ([RFC7662][3]).
* Resource Indicators for OAuth 2.0 ([RFC8707][5]).
* OAuth2 Token Revocation ([RFC7009][6]).
* [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)

## Installation

Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Changelog

* More robust error handling. When an error is emitted, you now give you access
to the emitted HTTP Response and response body.
* Support for `response_mode=fragment` in the `authorization_code` flow.


2.3.0 (2024-02-03)
Expand Down
29 changes: 26 additions & 3 deletions src/client/authorization-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ type GetAuthorizeUrlParams = {
* Any parameters listed here will be added to the query string for the authorization server endpoint.
*/
extraParams?: Record<string, string>;

/**
* By default response parameters for the authorization_flow will be added
* to the query string.
*
* Some servers let you put this in the fragment instead. This may be
* benefical if your client is a browser, as embedding the authorization
* code in the fragment part of the URI prevents it from being sent back
* to the server.
*
* See: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
*/
responseMode?: 'query' | 'fragment';
}

type ValidateResponseResult = {
Expand Down Expand Up @@ -114,6 +127,11 @@ export class OAuth2AuthorizationCodeClient {
if (params.resource) for(const resource of [].concat(params.resource as any)) {
query.append('resource', resource);
}

if (params.responseMode && params.responseMode!=='query') {
query.append('response_mode', params.responseMode);
}

if (params.extraParams) for(const [k,v] of Object.entries(params.extraParams)) {
if (query.has(k)) throw new Error(`Property in extraParams would overwrite standard property: ${k}`);
query.set(k, v);
Expand All @@ -125,7 +143,7 @@ export class OAuth2AuthorizationCodeClient {

async getTokenFromCodeRedirect(url: string|URL, params: Omit<GetTokenParams, 'code'> ): Promise<OAuth2Token> {

const { code } = await this.validateResponse(url, {
const { code } = this.validateResponse(url, {
state: params.state
});

Expand All @@ -144,9 +162,14 @@ export class OAuth2AuthorizationCodeClient {
* This function takes the url and validate the response. If the user
* redirected back with an error, an error will be thrown.
*/
async validateResponse(url: string|URL, params: {state?: string}): Promise<ValidateResponseResult> {
validateResponse(url: string|URL, params: {state?: string}): ValidateResponseResult {

const queryParams = new URL(url).searchParams;
url = new URL(url);
let queryParams = url.searchParams;
if (!queryParams.has('code') && !queryParams.has('error') && url.hash.length>0) {
// Try the fragment
queryParams = new URLSearchParams(url.hash.slice(1));
}

if (queryParams.has('error')) {
throw new OAuth2Error(
Expand Down
3 changes: 2 additions & 1 deletion src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type TokenResponse = {
}

type OAuth2ResponseType = 'code' | 'token';
type OAuth2ResponseMode = 'query' | 'fragment';
type OAuth2GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials' | 'refresh_token' | 'urn:ietf:params:oauth:grant-type:jwt-bearer' | 'urn:ietf:params:oauth:grant-type:saml2-bearer';
type OAuth2AuthMethod = 'none' | 'client_secret_basic' | 'client_secret_post' | 'client_secret_jwt' | 'private_key_jwt' | 'tls_client_auth' | 'self_signed_tls_client_auth';
type OAuth2CodeChallengeMethod = 'S256' | 'plain';
Expand Down Expand Up @@ -133,7 +134,7 @@ export type ServerMetadataResponse = {
* JSON array containing a list of the OAuth 2.0 "response_mode"
* values that this authorization server supports
*/
response_modes_supported?: string[];
response_modes_supported?: OAuth2ResponseMode[];

/**
* List of supported grant types by the server
Expand Down
53 changes: 53 additions & 0 deletions test/authorization-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,59 @@ describe('authorization-code', () => {

});

});


describe('validateResponse', () => {

const client = new OAuth2Client({
server: 'http://foo/',
tokenEndpoint: '/token',
clientId: 'test-client-id',
});

it('should correctly parse a valid URI from a OAUth2 server redirect', () => {

expect(
client.authorizationCode.validateResponse('https://example/?code=123&scope=scope1%20scope2', {})
).to.deep.equal({
code: '123',
scope: ['scope1', 'scope2']
});

});
it('should work when paramaters are set into the fragment', () => {

expect(
client.authorizationCode.validateResponse('https://example/#code=123&scope=scope1%20scope2', {})
).to.deep.equal({
code: '123',
scope: ['scope1', 'scope2']
});

});
it('should validate the state parameter', () => {

expect(
client.authorizationCode.validateResponse('https://example/?code=123&scope=scope1%20scope2&state=my-state', {state: 'my-state'})
).to.deep.equal({
code: '123',
scope: ['scope1', 'scope2']
});

});
it('should error if the state did not match', () => {

let caught = false;
try {
client.authorizationCode.validateResponse('https://example/?code=123&scope=scope1%20scope2', {state: 'my-state'});
} catch (err) {
caught = true;
}
expect(caught).to.equal(true);

});

});

});

0 comments on commit 39a7671

Please sign in to comment.