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

[scope-separator] Add support for custom scope separator #298

Merged
merged 1 commit into from
Jan 28, 2020
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
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