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

V3.1 catchup #629

Merged
merged 11 commits into from
Jun 30, 2020
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