Skip to content

Commit

Permalink
Merge pull request #298 from lelylan/feature/scope-separator
Browse files Browse the repository at this point in the history
[scope-separator] Add support for custom scope separator
  • Loading branch information
jonathansamines authored Jan 28, 2020
2 parents b74b1c2 + 4182fc6 commit 9eb41b9
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 13 deletions.
1 change: 1 addition & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion lib/access-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 9 additions & 7 deletions lib/grant-params.js
Original file line number Diff line number Diff line change
@@ -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),
};
}

Expand All @@ -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);
}
};
4 changes: 2 additions & 2 deletions lib/grants/authorization-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())}`;
}
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/grants/client-credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/grants/password-owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
31 changes: 31 additions & 0 deletions test/access_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
52 changes: 52 additions & 0 deletions test/authorization-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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',
Expand Down
27 changes: 27 additions & 0 deletions test/client-credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
32 changes: 32 additions & 0 deletions test/password-owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 9eb41b9

Please sign in to comment.