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

[encoding-mode] Add support for encoding mode on header credentials #300

Merged
merged 3 commits into from
Jan 29, 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
3 changes: 2 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ 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**.
* `scopeSeparator` Scope separator character. Some providers may require a different separator. Defaults to **empty space**
* `credentialsEncodingMode` Setup how credentials are encoded when `options.authorizationMode` is **header**. Use **loose** if your provider doesn't conform the [OAuth2 specification](https://tools.ietf.org/html/rfc6749#section-2.3.1). Defaults to **strict**
* `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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
## Next
### New features
* Add support to verify expired tokens with a custom expiration window (Useful to mitigate race conditions during the token refresh process)
* Add support to configure the encoding mode of header credentials by using `options.credentialsEncodingMode`

## 3.2.0
### New features
* [#298](https://github.com/lelylan/simple-oauth2/pull/298) Add support for custom scope separator by using the `options.scopeSeparator` configuration
* [#298](https://github.com/lelylan/simple-oauth2/pull/298) Add support for custom scope separator by using `options.scopeSeparator` configuration

### Improvements
* [#290](https://github.com/lelylan/simple-oauth2/pull/290) Valid token presence is verified on access token creation
Expand Down
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const optionsSchema = Joi
http: Joi.object().unknown(true),
options: Joi.object().keys({
scopeSeparator: Joi.string().default(' '),
bodyFormat: Joi.any().valid('form', 'json').default('form'),
credentialsEncodingMode: Joi.string().valid('strict', 'loose').default('strict'),
bodyFormat: Joi.string().valid('form', 'json').default('form'),
authorizationMethod: Joi.any().valid('header', 'body').default('header'),
}).default(),
});
Expand Down
28 changes: 25 additions & 3 deletions lib/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,45 @@ const HEADER_ENCODING_FORMAT = 'base64';
* while also applying some additional rules specified by the spec
*
* @see https://tools.ietf.org/html/rfc6749#appendix-B
* @see https://tools.ietf.org/html/rfc6749#section-2.3.1
*
* @param {String} value
*/
function useFormURLEncode(value) {
return encodeURIComponent(value).replace(/%20/g, '+');
}

module.exports = {
/**
* Get a string representation for the client credentials
*
* @param {String} clientID
* @param {String} clientSecret
* @returns {String} credentials
*/
function getCredentialsString(clientID, clientSecret) {
return `${clientID}:${clientSecret}`;
}

module.exports = class Encoding {
constructor(encodingMode) {
this.encodingMode = encodingMode;
}

/**
* Get the authorization header used to request a valid token
* @param {String} clientID
* @param {String} clientSecret
* @return {String} Authorization header string token
*/
getAuthorizationHeaderToken(clientID, clientSecret) {
const encodedCredentials = `${useFormURLEncode(clientID)}:${useFormURLEncode(clientSecret)}`;
let encodedCredentials;

if (this.encodingMode === 'strict') {
encodedCredentials = getCredentialsString(useFormURLEncode(clientID), useFormURLEncode(clientSecret));
} else {
encodedCredentials = getCredentialsString(clientID, clientSecret);
}

return Buffer.from(encodedCredentials).toString(HEADER_ENCODING_FORMAT);
},
}
};
3 changes: 2 additions & 1 deletion lib/request-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const Hoek = require('@hapi/hoek');
const querystring = require('querystring');
const debug = require('debug')('simple-oauth2:request-options');
const encoding = require('./encoding');
const Encoding = require('./encoding');

const JSON_CONTENT_TYPE = 'application/json';
const FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded';
Expand All @@ -25,6 +25,7 @@ module.exports = class RequestOptions {
const requestOptions = getDefaultRequestOptions();

if (this.config.options.authorizationMethod === 'header') {
const encoding = new Encoding(this.config.options.credentialsEncodingMode);
const credentials = encoding.getAuthorizationHeaderToken(this.config.client.id, this.config.client.secret);

debug('Using header authentication. Authorization header set to %s', credentials);
Expand Down
2 changes: 1 addition & 1 deletion test/access_token.js → test/access-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const Chance = require('chance');
const accessTokenMixin = require('chance-access-token');
const { isValid, isDate, differenceInSeconds } = require('date-fns');

const oauth2Module = require('./../index.js');
const oauth2Module = require('../index.js');
const { has, hasIn } = require('./_property');
const { createModuleConfig } = require('./_module-config');
const { createAuthorizationServer } = require('./_authorization-server-mock');
Expand Down
78 changes: 78 additions & 0 deletions test/authorization-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,84 @@ test.serial('@getToken => resolves to an access token (header credentials)', asy
t.deepEqual(token, getAccessToken());
});

test.serial('@getToken => resolves to an access token with custom module configuration (header credentials + loose encoding)', async (t) => {
const expectedRequestParams = {
grant_type: 'authorization_code',
code: 'code',
redirect_uri: 'http://callback.com',
};

const scopeOptions = getHeaderCredentialsScopeOptions({
reqheaders: {
Authorization: 'Basic dGhlICsgY2xpZW50ICsgaWQgJiBzeW1ib2xzOnRoZSArIGNsaWVudCArIHNlY3JldCAmIHN5bWJvbHM=',
},
});

const server = createAuthorizationServer('https://authorization-server.org:443');
const scope = server.tokenSuccess(scopeOptions, expectedRequestParams);

const config = createModuleConfig({
client: {
id: 'the + client + id & symbols',
secret: 'the + client + secret & symbols',
},
options: {
authorizationMethod: 'header',
credentialsEncodingMode: 'loose',
},
});

const tokenParams = {
code: 'code',
redirect_uri: 'http://callback.com',
};

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 with custom module configuration (header credentials + strict encoding)', async (t) => {
const expectedRequestParams = {
grant_type: 'authorization_code',
code: 'code',
redirect_uri: 'http://callback.com',
};

const scopeOptions = getHeaderCredentialsScopeOptions({
reqheaders: {
Authorization: 'Basic dGhlKyUyQitjbGllbnQrJTJCK2lkKyUyNitzeW1ib2xzOnRoZSslMkIrY2xpZW50KyUyQitzZWNyZXQrJTI2K3N5bWJvbHM=',
},
});

const server = createAuthorizationServer('https://authorization-server.org:443');
const scope = server.tokenSuccess(scopeOptions, expectedRequestParams);

const config = createModuleConfig({
client: {
id: 'the + client + id & symbols',
secret: 'the + client + secret & symbols',
},
options: {
authorizationMethod: 'header',
credentialsEncodingMode: 'strict',
},
});

const tokenParams = {
code: 'code',
redirect_uri: 'http://callback.com',
};

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 with custom module configuration (access token host and path)', async (t) => {
const expectedRequestParams = {
grant_type: 'authorization_code',
Expand Down
74 changes: 74 additions & 0 deletions test/client-credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,80 @@ test.serial('@getToken => resolves to an access token (header credentials)', asy
t.deepEqual(token, getAccessToken());
});

test.serial('@getToken => resolves to an access token with custom module configuration (header credentials + loose encoding)', async (t) => {
const expectedRequestParams = {
grant_type: 'client_credentials',
random_param: 'random value',
};

const scopeOptions = getHeaderCredentialsScopeOptions({
reqheaders: {
Authorization: 'Basic dGhlICsgY2xpZW50ICsgaWQgJiBzeW1ib2xzOnRoZSArIGNsaWVudCArIHNlY3JldCAmIHN5bWJvbHM=',
},
});

const server = createAuthorizationServer('https://authorization-server.org:443');
const scope = server.tokenSuccess(scopeOptions, expectedRequestParams);

const config = createModuleConfig({
client: {
id: 'the + client + id & symbols',
secret: 'the + client + secret & symbols',
},
options: {
authorizationMethod: 'header',
credentialsEncodingMode: 'loose',
},
});

const tokenParams = {
random_param: 'random value',
};

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 with custom module configuration (header credentials + strict encoding)', async (t) => {
const expectedRequestParams = {
grant_type: 'client_credentials',
random_param: 'random value',
};

const scopeOptions = getHeaderCredentialsScopeOptions({
reqheaders: {
Authorization: 'Basic dGhlKyUyQitjbGllbnQrJTJCK2lkKyUyNitzeW1ib2xzOnRoZSslMkIrY2xpZW50KyUyQitzZWNyZXQrJTI2K3N5bWJvbHM=',
},
});

const server = createAuthorizationServer('https://authorization-server.org:443');
const scope = server.tokenSuccess(scopeOptions, expectedRequestParams);

const config = createModuleConfig({
client: {
id: 'the + client + id & symbols',
secret: 'the + client + secret & symbols',
},
options: {
authorizationMethod: 'header',
credentialsEncodingMode: 'strict',
},
});

const tokenParams = {
random_param: 'random value',
};

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 with custom module configuration (access token host and path)', async (t) => {
const expectedRequestParams = {
grant_type: 'client_credentials',
Expand Down
78 changes: 78 additions & 0 deletions test/password-owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,84 @@ test.serial('@getToken => resolves to an access token (header credentials)', asy
t.deepEqual(token, getAccessToken());
});

test.serial('@getToken => resolves to an access token with custom module configuration (header credentials + loose encoding)', async (t) => {
const tokenRequestParams = {
grant_type: 'password',
username: 'alice',
password: 'secret',
};

const scopeOptions = getHeaderCredentialsScopeOptions({
reqheaders: {
Authorization: 'Basic dGhlICsgY2xpZW50ICsgaWQgJiBzeW1ib2xzOnRoZSArIGNsaWVudCArIHNlY3JldCAmIHN5bWJvbHM=',
},
});

const server = createAuthorizationServer('https://authorization-server.org:443');
const scope = server.tokenSuccess(scopeOptions, tokenRequestParams);

const config = createModuleConfig({
client: {
id: 'the + client + id & symbols',
secret: 'the + client + secret & symbols',
},
options: {
authorizationMethod: 'header',
credentialsEncodingMode: 'loose',
},
});

const tokenParams = {
username: 'alice',
password: 'secret',
};

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 with custom module configuration (header credentials + strict encoding)', async (t) => {
const tokenRequestParams = {
grant_type: 'password',
username: 'alice',
password: 'secret',
};

const scopeOptions = getHeaderCredentialsScopeOptions({
reqheaders: {
Authorization: 'Basic dGhlKyUyQitjbGllbnQrJTJCK2lkKyUyNitzeW1ib2xzOnRoZSslMkIrY2xpZW50KyUyQitzZWNyZXQrJTI2K3N5bWJvbHM=',
},
});

const server = createAuthorizationServer('https://authorization-server.org:443');
const scope = server.tokenSuccess(scopeOptions, tokenRequestParams);

const config = createModuleConfig({
client: {
id: 'the + client + id & symbols',
secret: 'the + client + secret & symbols',
},
options: {
authorizationMethod: 'header',
credentialsEncodingMode: 'strict',
},
});

const tokenParams = {
username: 'alice',
password: 'secret',
};

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 with custom module configuration (access token host and path)', async (t) => {
const tokenRequestParams = {
grant_type: 'password',
Expand Down