From 4182fc6a84e04e4e3cc76d1c96255bee36764486 Mon Sep 17 00:00:00 2001 From: Jonathan Samines Date: Mon, 27 Jan 2020 20:55:09 -0600 Subject: [PATCH] [scope-separator] Add support for custom scope separator configuration option --- API.md | 1 + CHANGELOG.md | 5 ++- index.js | 1 + lib/access-token.js | 2 +- lib/grant-params.js | 16 +++++----- lib/grants/authorization-code.js | 4 +-- lib/grants/client-credentials.js | 2 +- lib/grants/password-owner.js | 2 +- test/access_token.js | 31 +++++++++++++++++++ test/authorization-code.js | 52 ++++++++++++++++++++++++++++++++ test/client-credentials.js | 27 +++++++++++++++++ test/password-owner.js | 32 ++++++++++++++++++++ 12 files changed, 162 insertions(+), 13 deletions(-) diff --git a/API.md b/API.md index 1100f1f5..41edddd9 100644 --- a/API.md +++ b/API.md @@ -27,6 +27,7 @@ Simple OAuth2 accepts an object with the following params. * `authorization` Always overriden by the library to properly send the required credentials on each scenario * `options` additional options to setup how the module perform requests + * `scopeSeparator` Scope separator character. Some providers may require a different separator. Defaults to **empty space**. * `bodyFormat` - Request's body data format. Valid options are `form` or `json`. Defaults to **form** * `authorizationMethod` - Method used to send the *client.id*/*client.secret* authorization params at the token request. Valid options are `header` or `body`. If set to **body**, the **bodyFormat** option will be used to format the credentials. Defaults to **header** diff --git a/CHANGELOG.md b/CHANGELOG.md index 098eabd4..cc921c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## Next +### New features +* Add support for custom scope separator + ### Improvements * Valid token presence is verified on access token creation * Valid tokenType presence is verified on `.revoke` calls @@ -13,7 +16,7 @@ ## 3.1.0 ### New features -* [#277](https://github.com/lelylan/simple-oauth2/pull/277) Add support to parse expire at property on access tokens as UNIX timestamps +* [#277](https://github.com/lelylan/simple-oauth2/pull/277) Add support to parse access tokens's expire_at property as UNIX timestamps ## 3.0.1 ### Publishing changes diff --git a/index.js b/index.js index b3d72ddc..c10243d6 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,7 @@ const optionsSchema = Joi }).required(), http: Joi.object().unknown(true), options: Joi.object().keys({ + scopeSeparator: Joi.string().default(' '), bodyFormat: Joi.any().valid('form', 'json').default('form'), authorizationMethod: Joi.any().valid('header', 'body').default('header'), }).default(), diff --git a/lib/access-token.js b/lib/access-token.js index d9362073..b906b50e 100644 --- a/lib/access-token.js +++ b/lib/access-token.js @@ -72,7 +72,7 @@ module.exports = class AccessToken { refresh_token: this.token.refresh_token, }); - const parameters = GrantParams.forGrant(REFRESH_TOKEN_PROPERTY_NAME, refreshParams); + const parameters = GrantParams.forGrant(REFRESH_TOKEN_PROPERTY_NAME, this.config.options, refreshParams); const response = await this.client.request(this.config.auth.tokenPath, parameters.toObject()); return new AccessToken(this.config, this.client, response); diff --git a/lib/grant-params.js b/lib/grant-params.js index c14b0980..cc991b00 100644 --- a/lib/grant-params.js +++ b/lib/grant-params.js @@ -1,13 +1,13 @@ 'use strict'; -function getScopeParam(scope) { +function getScopeParam(scope, scopeSeparator) { if (scope === undefined) { return null; } if (Array.isArray(scope)) { return { - scope: scope.join(' '), + scope: scope.join(scopeSeparator), }; } @@ -17,21 +17,23 @@ function getScopeParam(scope) { } module.exports = class GrantParams { - static forGrant(grantType, params) { + static forGrant(grantType, options, params) { const baseParams = { grant_type: grantType, }; - return new GrantParams(baseParams, params); + return new GrantParams(options, baseParams, params); } - constructor(baseParams, params) { + constructor(options, baseParams, params) { + this.options = options; this.params = Object.assign({}, params); this.baseParams = Object.assign({}, baseParams); - this.scopeParams = getScopeParam(this.params.scope); } toObject() { - return Object.assign(this.baseParams, this.params, this.scopeParams); + const scopeParams = getScopeParam(this.params.scope, this.options.scopeSeparator); + + return Object.assign(this.baseParams, this.params, scopeParams); } }; diff --git a/lib/grants/authorization-code.js b/lib/grants/authorization-code.js index 043dbdb1..dce54300 100644 --- a/lib/grants/authorization-code.js +++ b/lib/grants/authorization-code.js @@ -27,7 +27,7 @@ module.exports = class AuthorizationCode { }; const url = new URL(this.config.auth.authorizePath, this.config.auth.authorizeHost); - const parameters = new GrantParams(baseParams, params); + const parameters = new GrantParams(this.config.options, baseParams, params); return `${url}?${querystring.stringify(parameters.toObject())}`; } @@ -42,7 +42,7 @@ module.exports = class AuthorizationCode { * @return {Promise} */ async getToken(params, httpOptions) { - const parameters = GrantParams.forGrant('authorization_code', params); + const parameters = GrantParams.forGrant('authorization_code', this.config.options, params); return this.client.request(this.config.auth.tokenPath, parameters.toObject(), httpOptions); } diff --git a/lib/grants/client-credentials.js b/lib/grants/client-credentials.js index 994bc78b..5bce20cc 100644 --- a/lib/grants/client-credentials.js +++ b/lib/grants/client-credentials.js @@ -17,7 +17,7 @@ module.exports = class ClientCredentials { * @return {Promise} */ async getToken(params, httpOptions) { - const parameters = GrantParams.forGrant('client_credentials', params); + const parameters = GrantParams.forGrant('client_credentials', this.config.options, params); return this.client.request(this.config.auth.tokenPath, parameters.toObject(), httpOptions); } diff --git a/lib/grants/password-owner.js b/lib/grants/password-owner.js index 2263184b..9c2c9fc8 100644 --- a/lib/grants/password-owner.js +++ b/lib/grants/password-owner.js @@ -19,7 +19,7 @@ module.exports = class PasswordOwner { * @return {Promise} */ async getToken(params, httpOptions) { - const parameters = GrantParams.forGrant('password', params); + const parameters = GrantParams.forGrant('password', this.config.options, params); return this.client.request(this.config.auth.tokenPath, parameters.toObject(), httpOptions); } diff --git a/test/access_token.js b/test/access_token.js index 5ae5764e..4467629c 100644 --- a/test/access_token.js +++ b/test/access_token.js @@ -265,6 +265,37 @@ test.serial('@refresh => creates a new access token with custom params', async ( t.true(has(refreshAccessToken.token, 'access_token')); }); +test.serial('@refresh => creates a new access token with custom module configuration (scope separator)', async (t) => { + const config = createModuleConfig({ + options: { + scopeSeparator: ',', + }, + }); + + const oauth2 = oauth2Module.create(config); + + const accessTokenResponse = chance.accessToken({ + expireMode: 'expires_in', + }); + + const refreshParams = { + grant_type: 'refresh_token', + scope: 'scope-a,scope-b', + refresh_token: accessTokenResponse.refresh_token, + }; + + const server = createAuthorizationServer('https://authorization-server.org:443'); + const scope = server.tokenSuccess(scopeOptions, refreshParams); + + const accessToken = oauth2.accessToken.create(accessTokenResponse); + const refreshAccessToken = await accessToken.refresh({ + scope: ['scope-a', 'scope-b'], + }); + + scope.done(); + t.true(has(refreshAccessToken.token, 'access_token')); +}); + test.serial('@refresh => creates a new access token with a custom token path', async (t) => { const config = createModuleConfig({ auth: { diff --git a/test/authorization-code.js b/test/authorization-code.js index c9b61aa0..50370e0b 100644 --- a/test/authorization-code.js +++ b/test/authorization-code.js @@ -53,6 +53,27 @@ test('@authorizeURL => returns the authorization URL with an scope array and def t.is(actual, expected); }); +test('@authorizeURL => returns the authorization URL with an scope array and a custom module configuration (scope separator)', (t) => { + const authorizeParams = { + redirect_uri: 'http://localhost:3000/callback', + state: '02afe928b', + scope: ['user', 'account'], + }; + + const config = createModuleConfig({ + options: { + scopeSeparator: ',', + }, + }); + + const oauth2 = oauth2Module.create(config); + + const actual = oauth2.authorizationCode.authorizeURL(authorizeParams); + const expected = `https://authorization-server.org/oauth/authorize?response_type=code&client_id=the%20client%20id&redirect_uri=${encodeURIComponent('http://localhost:3000/callback')}&state=02afe928b&scope=user%2Caccount`; + + t.is(actual, expected); +}); + test('@authorizeURL => returns the authorization URL with a custom module configuration (client id param name)', (t) => { const config = createModuleConfig({ client: { @@ -313,6 +334,37 @@ test.serial('@getToken => resolves to an access token with custom module configu t.deepEqual(token, getAccessToken()); }); +test.serial('@getToken => resolves to an access token with custom module configuration (scope separator)', async (t) => { + const expectedRequestParams = { + grant_type: 'authorization_code', + code: 'code', + redirect_uri: 'http://callback.com', + scope: 'scope-a,scope-b', + }; + + const scopeOptions = getHeaderCredentialsScopeOptions(); + const server = createAuthorizationServer('https://authorization-server.org:443'); + const scope = server.tokenSuccess(scopeOptions, expectedRequestParams); + + const config = createModuleConfig({ + options: { + scopeSeparator: ',', + }, + }); + + const tokenParams = { + code: 'code', + redirect_uri: 'http://callback.com', + scope: ['scope-a', 'scope-b'], + }; + + const oauth2 = oauth2Module.create(config); + const token = await oauth2.authorizationCode.getToken(tokenParams); + + scope.done(); + t.deepEqual(token, getAccessToken()); +}); + test.serial('@getToken => resolves to an access token while following redirections', async (t) => { const expectedRequestParams = { grant_type: 'authorization_code', diff --git a/test/client-credentials.js b/test/client-credentials.js index 4936c64a..4921017f 100644 --- a/test/client-credentials.js +++ b/test/client-credentials.js @@ -162,6 +162,33 @@ test.serial('@getToken => resolves to an access token with custom module configu t.deepEqual(token, getAccessToken()); }); +test.serial('@getToken = resolves to an access token with custom module configuration (scope separator)', async (t) => { + const expectedRequestParams = { + grant_type: 'client_credentials', + scope: 'scope-a,scope-b', + }; + + const scopeOptions = getHeaderCredentialsScopeOptions(); + const server = createAuthorizationServer('https://authorization-server.org:443'); + const scope = server.tokenSuccess(scopeOptions, expectedRequestParams); + + const tokenParams = { + scope: ['scope-a', 'scope-b'], + }; + + const config = createModuleConfig({ + options: { + scopeSeparator: ',', + }, + }); + + const oauth2 = oauth2Module.create(config); + const token = await oauth2.clientCredentials.getToken(tokenParams); + + scope.done(); + t.deepEqual(token, getAccessToken()); +}); + test.serial('@getToken => resolves to an access token while following redirections', async (t) => { const expectedRequestParams = { grant_type: 'client_credentials', diff --git a/test/password-owner.js b/test/password-owner.js index 045f8117..99a1ccb9 100644 --- a/test/password-owner.js +++ b/test/password-owner.js @@ -172,6 +172,38 @@ test.serial('@getToken => resolves to an access token with custom module configu t.deepEqual(token, getAccessToken()); }); +test.serial('@getToken => resolves to an access token with custom module configuration (token separator)', async (t) => { + const tokenRequestParams = { + grant_type: 'password', + username: 'alice', + password: 'secret', + scope: 'scope-a,scope-b', + }; + + const scopeOptions = getHeaderCredentialsScopeOptions(); + const server = createAuthorizationServer('https://authorization-server.org:443'); + const scope = server.tokenSuccess(scopeOptions, tokenRequestParams); + + const tokenParams = { + username: 'alice', + password: 'secret', + scope: ['scope-a', 'scope-b'], + }; + + const config = createModuleConfig({ + options: { + scopeSeparator: ',', + }, + }); + + const oauth2 = oauth2Module.create(config); + + const token = await oauth2.ownerPassword.getToken(tokenParams); + + scope.done(); + t.deepEqual(token, getAccessToken()); +}); + test.serial('@getToken => resolves to an access token while following redirections', async (t) => { const tokenRequestParams = { grant_type: 'password',