Skip to content
Merged
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,35 @@ node_modules/
docs/_build/
__pycache__/
*.pyc
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.iml

.idea
.jshint
.DS_Store

pids
logs
results

lib/dockerImage/keys
coverage
npm-debug.log*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
.elc
auto-save-list
tramp
.\#*

# Org-mode
.org-id-locations
*_archive
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
## Changelog

### 3.1.0
* new: .npmignore tests
* fix: validate requested scope on authorize request
* fix: always issue correct expiry dates for tokens
* fix: set numArgs for promisify of generateAuthorizationCode
* fix: Changed 'hasOwnProperty' call in Response
* docs: Ensure accessTokenExpiresAt is required
* docs: Add missing notice of breaking change for accessExpireLifetime to migration guide
* docs: Correct tokens time scale for 2.x to 3.x migration guide
* readme: Update Slack badge and link
* readme: Fix link to RFC6750 standard

### 3.0.2 (24/05/2020)

* Update all dependencies 🎉
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ npm test
[travis-url]: https://travis-ci.org/oauthjs/node-oauth2-server
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
[license-url]: https://raw.githubusercontent.com/oauthjs/node-oauth2-server/master/LICENSE
[slack-image]: https://img.shields.io/badge/slack-join-E01563.svg
[slack-url]: https://oauthjs.slack.com
[slack-image]: https://slack.oauthjs.org/badge.svg
[slack-url]: https://slack.oauthjs.org

18 changes: 9 additions & 9 deletions docs/misc/migrating-v2-to-v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ The naming of the exposed middlewares has changed to match the OAuth2 _RFC_ more
Server options
--------------

The following server options can be set when instantiating the OAuth service:
The following server options can be set when instantiating the OAuth service:

* `addAcceptedScopesHeader`: **default true** Add the `X-Accepted-OAuth-Scopes` header with a list of scopes that will be accepted
* `addAuthorizedScopesHeader`: **default true** Add the `X-OAuth-Scopes` header with a list of scopes that the user is authorized for
* `allowBearerTokensInQueryString`: **default false** Determine if the bearer token can be included in the query string (i.e. `?access_token=`) for validation calls
* `allowEmptyState`: **default false** If true, `state` can be empty or not passed. If false, `state` is required.
* `authorizationCodeLifetime`: **default 300** Default number of milliseconds that the authorization code is active for
* `accessTokenLifetime`: **default 3600** Default number of milliseconds that an access token is valid for
* `refreshTokenLifetime`: **default 1209600** Default number of milliseconds that a refresh token is valid for
* `authorizationCodeLifetime`: **default 300** Default number of seconds that the authorization code is active for
* `accessTokenLifetime`: **default 3600** Default number of seconds that an access token is valid for
* `refreshTokenLifetime`: **default 1209600** Default number of seconds that a refresh token is valid for
* `allowExtendedTokenAttributes`: **default false** Allows additional attributes (such as `id_token`) to be included in token responses.
* `requireClientAuthentication`: **default true for all grant types** Allow ability to set client/secret authentication to `false` for a specific grant type.
* `requireClientAuthentication`: **default true for all grant types** Allow ability to set client/secret authentication to `false` for a specific grant type.

The following server options have changed behavior in v3.0.0:

Expand All @@ -60,7 +60,7 @@ Model specification
* `generateAuthorizationCode()` is **optional** and should return a `String`.
* `generateRefreshToken(client, user, scope)` is **optional** and should return a `String`.
* `getAccessToken(token)` should return an object with:

* `accessToken` (`String`)
* `accessTokenExpiresAt` (`Date`)
* `client` (`Object`), containing at least an `id` property that matches the supplied client
Expand All @@ -75,7 +75,7 @@ Model specification
* `user` (`Object`)

* `getClient(clientId, clientSecret)` should return an object with, at minimum:

* `redirectUris` (`Array`)
* `grants` (`Array`)

Expand All @@ -88,11 +88,11 @@ Model specification
* `user` (`Object`)

* `getUser(username, password)` should return an object:

* No longer requires that `id` be returned.

* `getUserFromClient(client)` should return an object:

* No longer requires that `id` be returned.

* `grantTypeAllowed()` was **removed**. You can instead:
Expand Down
2 changes: 1 addition & 1 deletion docs/model/spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ An ``Object`` representing the access token and associated data.
+------------------------------+--------+--------------------------------------------------+
| token.accessToken | String | The access token passed to ``getAccessToken()``. |
+------------------------------+--------+--------------------------------------------------+
| [token.accessTokenExpiresAt] | Date | The expiry time of the access token. |
| token.accessTokenExpiresAt | Date | The expiry time of the access token. |
+------------------------------+--------+--------------------------------------------------+
| [token.scope] | String | The authorized scope of the access token. |
+------------------------------+--------+--------------------------------------------------+
Expand Down
12 changes: 2 additions & 10 deletions lib/grant-types/abstract-grant-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,15 @@ AbstractGrantType.prototype.generateRefreshToken = function(client, user, scope)
*/

AbstractGrantType.prototype.getAccessTokenExpiresAt = function() {
var expires = new Date();

expires.setSeconds(expires.getSeconds() + this.accessTokenLifetime);

return expires;
return new Date(Date.now() + this.accessTokenLifetime * 1000);
};

/**
* Get refresh token expiration date.
*/

AbstractGrantType.prototype.getRefreshTokenExpiresAt = function() {
var expires = new Date();

expires.setSeconds(expires.getSeconds() + this.refreshTokenLifetime);

return expires;
return new Date(Date.now() + this.refreshTokenLifetime * 1000);
};

/**
Expand Down
33 changes: 28 additions & 5 deletions lib/handlers/authorize-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,16 @@ AuthorizeHandler.prototype.handle = function(request, response) {
var ResponseType;

return Promise.bind(this)
.then(function() {
scope = this.getScope(request);
.then(function() {
var requestedScope = this.getScope(request);

return this.generateAuthorizationCode(client, user, scope);
})
return this.validateScope(user, client, requestedScope);
})
.then(function(validScope) {
scope = validScope;

return this.generateAuthorizationCode(client, user, scope);
})
.then(function(authorizationCode) {
state = this.getState(request);
ResponseType = this.getResponseType(request);
Expand Down Expand Up @@ -135,7 +140,7 @@ AuthorizeHandler.prototype.handle = function(request, response) {

AuthorizeHandler.prototype.generateAuthorizationCode = function(client, user, scope) {
if (this.model.generateAuthorizationCode) {
return promisify(this.model.generateAuthorizationCode).call(this.model, client, user, scope);
return promisify(this.model.generateAuthorizationCode, 3).call(this.model, client, user, scope);
}
return tokenUtil.generateRandomToken();
};
Expand Down Expand Up @@ -196,6 +201,24 @@ AuthorizeHandler.prototype.getClient = function(request) {
});
};

/**
* Validate requested scope.
*/
AuthorizeHandler.prototype.validateScope = function(user, client, scope) {
if (this.model.validateScope) {
return promisify(this.model.validateScope, 3).call(this.model, user, client, scope)
.then(function (scope) {
if (!scope) {
throw new InvalidScopeError('Invalid scope: Requested scope is invalid');
}

return scope;
});
} else {
return Promise.resolve(scope);
}
};

/**
* Get scope from the request.
*/
Expand Down
4 changes: 2 additions & 2 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ function Request(options) {

// Store the headers in lower case.
for (var field in options.headers) {
if (options.headers.hasOwnProperty(field)) {
if (Object.prototype.hasOwnProperty.call(options.headers, field)) {
this.headers[field.toLowerCase()] = options.headers[field];
}
}

// Store additional properties of the request object passed in
for (var property in options) {
if (options.hasOwnProperty(property) && !this[property]) {
if (Object.prototype.hasOwnProperty.call(options, property) && !this[property]) {
this[property] = options[property];
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ function Response(options) {

// Store the headers in lower case.
for (var field in options.headers) {
if (options.headers.hasOwnProperty(field)) {
if (Object.prototype.hasOwnProperty.call(options.headers, field)) {
this.headers[field.toLowerCase()] = options.headers[field];
}
}

// Store additional properties of the response object passed in
for (var property in options) {
if (options.hasOwnProperty(property) && !this[property]) {
if (Object.prototype.hasOwnProperty.call(options, property) && !this[property]) {
this[property] = options[property];
}
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "oauth2-server",
"description": "Complete, framework-agnostic, compliant and well tested module for implementing an OAuth2 Server in node.js",
"version": "3.0.2",
"version": "3.1.0-rc1",
"keywords": [
"oauth",
"oauth2"
Expand Down
88 changes: 88 additions & 0 deletions test/integration/handlers/authorize-handler_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,94 @@ describe('AuthorizeHandler integration', function() {
});
});

it('should redirect to a successful response if `model.validateScope` is not defined', function() {
var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] };
var model = {
getAccessToken: function() {
return {
client: client,
user: {},
accessTokenExpiresAt: new Date(new Date().getTime() + 10000)
};
},
getClient: function() {
return client;
},
saveAuthorizationCode: function() {
return { authorizationCode: 12345, client: client };
}
};
var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
var request = new Request({
body: {
client_id: 12345,
response_type: 'code'
},
headers: {
'Authorization': 'Bearer foo'
},
method: {},
query: {
scope: 'read',
state: 'foobar'
}
});
var response = new Response({ body: {}, headers: {} });

return handler.handle(request, response)
.then(function(data) {
data.should.eql({
authorizationCode: 12345,
client: client
});
})
.catch(should.fail);
});

it('should redirect to an error response if `scope` is insufficient', function() {
var client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] };
var model = {
getAccessToken: function() {
return {
client: client,
user: {},
accessTokenExpiresAt: new Date(new Date().getTime() + 10000)
};
},
getClient: function() {
return client;
},
saveAuthorizationCode: function() {
return { authorizationCode: 12345, client: client };
},
validateScope: function() {
return false;
}
};
var handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model });
var request = new Request({
body: {
client_id: 12345,
response_type: 'code'
},
headers: {
'Authorization': 'Bearer foo'
},
method: {},
query: {
scope: 'read',
state: 'foobar'
}
});
var response = new Response({ body: {}, headers: {} });

return handler.handle(request, response)
.then(should.fail)
.catch(function() {
response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid');
});
});

it('should redirect to an error response if `state` is missing', function() {
var model = {
getAccessToken: function() {
Expand Down