From 8ebe219b337cb86d44caa91a9740527c6e823d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Mon, 19 Jan 2015 12:35:58 -0800 Subject: [PATCH 01/21] Implements scope support Resolves https://github.com/thomseddon/node-oauth2-server#49 by obtaining an optional scope parameter from a grant request, which is passed to the model for processing and storage. The authorization flow includes a call to `checkScope` which receives the access token and the request object. In order to support the access code grant flow, the scope param is passed to `saveAuthCode` so that it may be persisted for a subsequent token exchange. --- lib/authCodeGrant.js | 6 ++++-- lib/authorise.js | 18 +++++++++++++++++- lib/error.js | 1 + lib/grant.js | 22 +++++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/authCodeGrant.js b/lib/authCodeGrant.js index 616bfbdeb..cd5a33710 100644 --- a/lib/authCodeGrant.js +++ b/lib/authCodeGrant.js @@ -137,7 +137,7 @@ function checkClient (done) { */ function checkUserApproved (done) { var self = this; - this.check(this.req, function (err, allowed, user) { + this.check(this.req, function (err, allowed, user, scope) { if (err) return done(error('server_error', false, err)); if (!allowed) { @@ -146,6 +146,8 @@ function checkUserApproved (done) { } self.user = user; + self.scope = scope; + done(); }); } @@ -175,7 +177,7 @@ function saveAuthCode (done) { expires.setSeconds(expires.getSeconds() + this.config.authCodeLifetime); this.model.saveAuthCode(this.authCode, this.client.clientId, expires, - this.user, function (err) { + this.user, this.scope, function (err) { if (err) return done(error('server_error', false, err)); done(); }); diff --git a/lib/authorise.js b/lib/authorise.js index 2b8296019..cc91bfdf7 100644 --- a/lib/authorise.js +++ b/lib/authorise.js @@ -26,7 +26,8 @@ module.exports = Authorise; */ var fns = [ getBearerToken, - checkToken + checkToken, + checkScope ]; /** @@ -128,3 +129,18 @@ function checkToken (done) { done(); }); } + +/** + * Check scope + * + * Passes request information to the model for scope validation + * @param {Function} done + * @this OAuth + */ +function checkScope (done) { + this.model.checkScope(this.bearerToken, this.req, function (err) { + if (err) return done(error('invalid_scope', err)); + + done(); + }); +} diff --git a/lib/error.js b/lib/error.js index 04d181ee8..36e22aa0e 100644 --- a/lib/error.js +++ b/lib/error.js @@ -46,6 +46,7 @@ function OAuth2Error (error, description, err) { 'WWW-Authenticate': 'Basic realm="Service"' }; /* falls through */ + case 'invalid_scope': case 'invalid_grant': case 'invalid_request': this.code = 400; diff --git a/lib/grant.js b/lib/grant.js index 7ceb6c640..e24ead11d 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -36,6 +36,7 @@ var fns = [ saveAccessToken, generateRefreshToken, saveRefreshToken, + saveScope, sendResponse ]; @@ -193,6 +194,7 @@ function useAuthCodeGrant (done) { } self.user = authCode.user || { id: authCode.userId }; + self.scope = authCode.scope; if (!self.user.id) { return done(error('server_error', false, 'No user/userId parameter returned from getauthCode')); @@ -449,7 +451,24 @@ function saveRefreshToken (done) { } /** - * Create an access token and save it with the model + * Pass the scope string to the model for saving + * + * @param {Function} done + * @this OAuth + */ +function saveScope (done) { + var scope = this.scope || this.req.body.scope; + + var self = this; + this.model.saveScope(scope, self.accessToken, function (err, acceptedScope) { + if (err) return done(error('server_error', false, err)); + self.scope = acceptedScope; + done(); + }); +} + +/** + * Sends the resulting token(s) and related information to the client * * @param {Function} done * @this OAuth @@ -457,6 +476,7 @@ function saveRefreshToken (done) { function sendResponse (done) { var response = { token_type: 'bearer', + scope: this.scope, access_token: this.accessToken }; From c199992aa93e02142526f0b4ee8b791150cdef11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Fri, 23 Jan 2015 04:43:43 -0800 Subject: [PATCH 02/21] Scoping is now implemented using middleware Custom middleware allows for defining scope key requirements on routers or single routes. If a scope requirement is set, the model's checkScope() method is tasked with verifying whether or not the access token possesses the required key(s). --- lib/authorise.js | 18 +------------ lib/oauth2server.js | 21 ++++++++++++++- lib/scope.js | 65 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 lib/scope.js diff --git a/lib/authorise.js b/lib/authorise.js index cc91bfdf7..2b8296019 100644 --- a/lib/authorise.js +++ b/lib/authorise.js @@ -26,8 +26,7 @@ module.exports = Authorise; */ var fns = [ getBearerToken, - checkToken, - checkScope + checkToken ]; /** @@ -129,18 +128,3 @@ function checkToken (done) { done(); }); } - -/** - * Check scope - * - * Passes request information to the model for scope validation - * @param {Function} done - * @this OAuth - */ -function checkScope (done) { - this.model.checkScope(this.bearerToken, this.req, function (err) { - if (err) return done(error('invalid_scope', err)); - - done(); - }); -} diff --git a/lib/oauth2server.js b/lib/oauth2server.js index 2726043b6..44a459544 100644 --- a/lib/oauth2server.js +++ b/lib/oauth2server.js @@ -17,7 +17,8 @@ var error = require('./error'), AuthCodeGrant = require('./authCodeGrant'), Authorise = require('./authorise'), - Grant = require('./grant'); + Grant = require('./grant'), + Scope = require('./scope'); module.exports = OAuth2Server; @@ -104,6 +105,24 @@ OAuth2Server.prototype.authCodeGrant = function (check) { }; }; +/** + * Scope Check Middleware + * + * Returns middleware that allows the specification of required + * scope(s) for routers and/or routes, which is validated by the model. + * + * @param {Mixed} requiredScope String or list of scope keys + * required to access the route. + * @return {Function} + */ +OAuth2Server.prototype.scope = function(requiredScope) { + var self = this; + + return function(req, res, next) { + return new Scope(self, req, next, requiredScope); + }; +}; + /** * OAuth Error Middleware * diff --git a/lib/scope.js b/lib/scope.js new file mode 100644 index 000000000..e369ed369 --- /dev/null +++ b/lib/scope.js @@ -0,0 +1,65 @@ +/** + * Copyright 2013-present NightWorld. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var error = require('./error'), + runner = require('./runner'); + +module.exports = Scope; + +/** + * This is the function order used by the runner + * + * @type {Array} + */ +var fns = [ + checkScope +]; + +/** + * Scope + * + * @param {Object} config Instance of OAuth object + * @param {Object} req + * @param {Function} next + * @param {Mixed} requiredScope String or list of scope keys required + * to access the resource + */ +function Scope (config, req, next, requiredScope) { + this.config = config; + this.model = config.model; + this.req = req; + this.requiredScope = requiredScope; + + runner(fns, this, next); +} + +/** + * Check scope + * + * Passes the access token and required scope key(s) to the model + * for access validation. Bad requests are rejected with an + * invalid_scope error. + * + * @param {Function} done + * + */ +function checkScope (done) { + this.model.checkScope(this.req.oauth.bearerToken.accessToken, + this.requiredScope, function (err) { + if (err) { return done(new error('invalid_scope', err)); } + done(); + }); +} From ddac89f48193936cc605f2668d360ccead05a511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Mon, 26 Jan 2015 13:13:18 -0800 Subject: [PATCH 03/21] The checkScope method now receives the entire bearerToken object This change allows for simpler access to the scope information, should the developer chose to include it in the getAccessToken response. --- lib/grant.js | 3 ++- lib/scope.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/grant.js b/lib/grant.js index e24ead11d..0704c50ca 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -194,7 +194,7 @@ function useAuthCodeGrant (done) { } self.user = authCode.user || { id: authCode.userId }; - self.scope = authCode.scope; + self.scope = authCode.scope || ''; if (!self.user.id) { return done(error('server_error', false, 'No user/userId parameter returned from getauthCode')); @@ -259,6 +259,7 @@ function useRefreshTokenGrant (done) { } self.user = refreshToken.user || { id: refreshToken.userId }; + self.scope = refreshToken.scope || ''; if (self.model.revokeRefreshToken) { return self.model.revokeRefreshToken(token, function (err) { diff --git a/lib/scope.js b/lib/scope.js index e369ed369..c678c188f 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -57,7 +57,7 @@ function Scope (config, req, next, requiredScope) { * */ function checkScope (done) { - this.model.checkScope(this.req.oauth.bearerToken.accessToken, + this.model.checkScope(this.req.oauth.bearerToken, this.requiredScope, function (err) { if (err) { return done(new error('invalid_scope', err)); } done(); From 4e281d67b34fa73c1cb09064d6913979d26cb441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Tue, 27 Jan 2015 17:19:47 -0800 Subject: [PATCH 04/21] Updated Readme and postgresql example Added method descriptors for saveScope and checkScope to the Readme; added a scope param to saveAuthCode, since the user-accepted string must be passed along server-side to the token creation step. Corresponding updates were made to the postgresql example, which now includes a simple usage scenario with a scope string being stored in the oauth_access_tokens table. --- Readme.md | 25 +++++++++++++++--- examples/postgresql/index.js | 6 +++++ examples/postgresql/model.js | 47 ++++++++++++++++++++++++++++------ examples/postgresql/schema.sql | 1 + 4 files changed, 67 insertions(+), 12 deletions(-) diff --git a/Readme.md b/Readme.md index 37352ec8f..7d8801e02 100644 --- a/Readme.md +++ b/Readme.md @@ -133,6 +133,20 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *mixed* **error** - Truthy to indicate an error +#### checkScope (accessToken, requiredScope, callback) +- *string* **accessToken** +- *mixed* **requiredScope** + - String, array, or object indicating which scope(s) a token must possess +- *function* **callback (error)** + - *mixed* **error** + - Truthy to indicate an error + +#### saveScope (scope, accessToken, callback) +- *string* **scope** +- *object* **accessToken** +- *function* **callback (error)** + - *mixed* **error** + - Truthy to indicate an error ### Required for `authorization_code` grant type @@ -151,12 +165,13 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *string|number* **userId** - The userId -#### saveAuthCode (authCode, clientId, expires, user, callback) +#### saveAuthCode (authCode, clientId, expires, user, scope, callback) - *string* **authCode** - *string* **clientId** - *date* **expires** - *mixed* **user** - Whatever was passed as `user` to the codeGrant function (see example) +- *string* **scope** - *function* **callback (error)** - *mixed* **error** - Truthy to indicate an error @@ -275,7 +290,7 @@ First you must insert client id/secret and user into storage. This is out of the To obtain a token you should POST to `/oauth/token`. You should include your client credentials in the Authorization header ("Basic " + client_id:client_secret base64'd), and then grant_type ("password"), -username and password in the request body, for example: +username, password, and optionally a scope in the request body, for example: ``` POST /oauth/token HTTP/1.1 @@ -283,7 +298,7 @@ Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded -grant_type=password&username=johndoe&password=A3ddj3w +grant_type=password&username=johndoe&password=A3ddj3w&scope=readonly ``` This will then call the following on your model (in this order): - getClient (clientId, clientSecret, callback) @@ -291,6 +306,7 @@ This will then call the following on your model (in this order): - getUser (username, password, callback) - saveAccessToken (accessToken, clientId, expires, user, callback) - saveRefreshToken (refreshToken, clientId, expires, user, callback) **(if using)** + - saveScope (scopeString, accessToken, callback) Provided there weren't any errors, this will return the following (excluding the `refresh_token` if you've not enabled the refresh_token grant type): @@ -304,7 +320,8 @@ Pragma: no-cache "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"bearer", "expires_in":3600, - "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" + "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", + "scope": "readonly" } ``` diff --git a/examples/postgresql/index.js b/examples/postgresql/index.js index e1cf18e5e..f1816be2e 100644 --- a/examples/postgresql/index.js +++ b/examples/postgresql/index.js @@ -75,6 +75,12 @@ app.get('/secret', app.oauth.authorise(), function (req, res) { res.send('Secret area'); }); +app.get('/scoped', app.oauth.authorise(), app.oauth.scope('demo'), + function (req, res) { + // Will require that the access_token possesses the 'demo' scope key + res.send('Secret and scope-controlled area'); +}); + app.get('/public', function (req, res) { // Does not require an access_token res.send('Public area'); diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index 31df5369c..ec97b40c4 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -25,7 +25,7 @@ var pg = require('pg'), model.getAccessToken = function (bearerToken, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - client.query('SELECT access_token, client_id, expires, user_id FROM oauth_access_tokens ' + + client.query('SELECT access_token, scope, client_id, expires, user_id FROM oauth_access_tokens ' + 'WHERE access_token = $1', [bearerToken], function (err, result) { if (err || !result.rowCount) return callback(err); // This object will be exposed in req.oauth.token @@ -37,7 +37,8 @@ model.getAccessToken = function (bearerToken, callback) { accessToken: token.access_token, clientId: token.client_id, expires: token.expires, - userId: token.userId + userId: token.userId, + scope: token.scope.split(' ') // Assumes a flat, space-separated scope string }); done(); }); @@ -69,12 +70,14 @@ model.getClient = function (clientId, clientSecret, callback) { model.getRefreshToken = function (bearerToken, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - client.query('SELECT refresh_token, client_id, expires, user_id FROM oauth_refresh_tokens ' + - 'WHERE refresh_token = $1', [bearerToken], function (err, result) { - // The returned user_id will be exposed in req.user.id - callback(err, result.rowCount ? result.rows[0] : false); - done(); - }); + // Note: To avoid replicating the scope string in both token tables, the old + // access token's scope string must be retrieved and passed along from here. + client.query('SELECT rt.refresh_token, rt.client_id, rt.expires, rt.user_id, at.scope FROM ' + + 'oauth_refresh_tokens AS rt, oauth_access_tokens AS at WHERE rt.user_id = ' + + 'at.user_id AND rt.client_id = at.client_id AND rt.refresh_token = $', + [bearerToken], function (err, result) { + callback(err, result.rowCount ? result.rows[0] : false); + }); }); }; @@ -113,6 +116,34 @@ model.saveRefreshToken = function (refreshToken, clientId, expires, userId, call }); }; +model.saveScope = function (scope, accessToken, callback) { + // Here you will want to validate that what the client is soliciting + // makes sense. You might then proceed by storing the validated scope. + // In this example, the scope is simly stored as a string in the + // oauth_access_tokens table, but you could also handle them as entries + // in a connection table. + var acceptedScope = scope; + + pg.connect(connString, function (err, client, done) { + if (err) return callback(err); + client.query('UPDATE oauth_access_tokens SET scope=$1 WHERE access_token = $2', + [acceptedScope, accessToken], function (err, result) { + callback(err, acceptedScope); + done(); + }); +}; + +model.checkScope = function (accessToken, requiredScope, callback) + // requiredScope is set through the scope middleware. + // You may pass anything from a simple string, as this example illustrates, + // to representations including scopes and subscopes such as + // { "account": [ "edit" ] } + if(accessToken.scope.indexOf(requiredScope) === -1) { + return callback('Required scope: ' + requiredScope); + } + callback(); +}; + /* * Required to support password grant type */ diff --git a/examples/postgresql/schema.sql b/examples/postgresql/schema.sql index d31212a8a..9fd6c1f61 100644 --- a/examples/postgresql/schema.sql +++ b/examples/postgresql/schema.sql @@ -34,6 +34,7 @@ SET default_with_oids = false; CREATE TABLE oauth_access_tokens ( access_token text NOT NULL, + scope text NOT NULL, client_id text NOT NULL, user_id uuid NOT NULL, expires timestamp without time zone NOT NULL From fd69c79043915f3c69713936e5862b480c49afda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Thu, 29 Jan 2015 12:29:35 -0800 Subject: [PATCH 05/21] Documentation fixes and checkScope parameter order changed --- Readme.md | 8 ++++---- examples/postgresql/model.js | 2 +- lib/scope.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Readme.md b/Readme.md index 7d8801e02..0c3223322 100644 --- a/Readme.md +++ b/Readme.md @@ -133,10 +133,10 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *mixed* **error** - Truthy to indicate an error -#### checkScope (accessToken, requiredScope, callback) -- *string* **accessToken** -- *mixed* **requiredScope** +#### checkScope (scope, accessToken, callback) +- *mixed* **scope** - String, array, or object indicating which scope(s) a token must possess +- *string* **accessToken** - *function* **callback (error)** - *mixed* **error** - Truthy to indicate an error @@ -306,7 +306,7 @@ This will then call the following on your model (in this order): - getUser (username, password, callback) - saveAccessToken (accessToken, clientId, expires, user, callback) - saveRefreshToken (refreshToken, clientId, expires, user, callback) **(if using)** - - saveScope (scopeString, accessToken, callback) + - saveScope (scope, accessToken, callback) Provided there weren't any errors, this will return the following (excluding the `refresh_token` if you've not enabled the refresh_token grant type): diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index ec97b40c4..a29e2ed82 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -119,7 +119,7 @@ model.saveRefreshToken = function (refreshToken, clientId, expires, userId, call model.saveScope = function (scope, accessToken, callback) { // Here you will want to validate that what the client is soliciting // makes sense. You might then proceed by storing the validated scope. - // In this example, the scope is simly stored as a string in the + // In this example, the scope is simply stored as a string in the // oauth_access_tokens table, but you could also handle them as entries // in a connection table. var acceptedScope = scope; diff --git a/lib/scope.js b/lib/scope.js index c678c188f..e0db7f60b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -57,8 +57,8 @@ function Scope (config, req, next, requiredScope) { * */ function checkScope (done) { - this.model.checkScope(this.req.oauth.bearerToken, - this.requiredScope, function (err) { + this.model.checkScope(this.requiredScope, + this.req.oauth.bearerToken, function (err) { if (err) { return done(new error('invalid_scope', err)); } done(); }); From 01450c39deec3cb6497792e9c42e450eec2e8607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Tue, 3 Feb 2015 16:15:06 -0800 Subject: [PATCH 06/21] Added optional scope argument to authorise middleware In addition to the dedicated scope middleware, required scope may now also be defined through the authorise middleware. Multiple scope definitions cascade, meaning that it is possible to protect a router instance via authorise, and add finer-grained requirements for individual routes. --- lib/authorise.js | 15 ++++++++++++--- lib/oauth2server.js | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/authorise.js b/lib/authorise.js index 2b8296019..b240eb73a 100644 --- a/lib/authorise.js +++ b/lib/authorise.js @@ -32,15 +32,17 @@ var fns = [ /** * Authorise * - * @param {Object} config Instance of OAuth object + * @param {Object} config Instance of OAuth object * @param {Object} req * @param {Object} res + * @param {Object} options May indicate required scope(s) * @param {Function} next */ -function Authorise (config, req, next) { +function Authorise (config, req, scope, next) { this.config = config; this.model = config.model; this.req = req; + this.scope = scope; runner(fns, this, next); } @@ -125,6 +127,13 @@ function checkToken (done) { self.req.oauth = { bearerToken: token }; self.req.user = token.user ? token.user : { id: token.userId }; - done(); + if(self.scope) { + self.model.checkScope(self.scope, token, function (err) { + if (err) { return done(new error('invalid_scope', err)); } + done(); + }); + } else { + done(); + } }); } diff --git a/lib/oauth2server.js b/lib/oauth2server.js index 44a459544..90f206641 100644 --- a/lib/oauth2server.js +++ b/lib/oauth2server.js @@ -64,11 +64,11 @@ function OAuth2Server (config) { * * @return {Function} middleware */ -OAuth2Server.prototype.authorise = function () { +OAuth2Server.prototype.authorise = function (scope) { var self = this; return function (req, res, next) { - return new Authorise(self, req, next); + return new Authorise(self, req, scope, next); }; }; From eec2eed660414cd9050a7c5511acd50cbd9b1d61 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 10 Feb 2015 15:51:17 +0000 Subject: [PATCH 07/21] Fix parameter order in postgres model --- examples/postgresql/model.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index a29e2ed82..fd634a5a4 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -133,13 +133,12 @@ model.saveScope = function (scope, accessToken, callback) { }); }; -model.checkScope = function (accessToken, requiredScope, callback) - // requiredScope is set through the scope middleware. +model.checkScope = function (scope, accessToken, callback) // You may pass anything from a simple string, as this example illustrates, // to representations including scopes and subscopes such as // { "account": [ "edit" ] } - if(accessToken.scope.indexOf(requiredScope) === -1) { - return callback('Required scope: ' + requiredScope); + if(accessToken.scope.indexOf(scope) === -1) { + return callback('Required scope: ' + scope); } callback(); }; From ceb8fab1da51de0a4c8faa1c3679641a29dd4ad0 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 10 Feb 2015 16:05:20 +0000 Subject: [PATCH 08/21] Improve scope support in authorise middleware --- Readme.md | 9 ++- examples/postgresql/model.js | 9 +-- lib/authorise.js | 39 +++++++--- lib/oauth2server.js | 4 +- test/authorise.js | 143 ++++++++++++++++++----------------- 5 files changed, 114 insertions(+), 90 deletions(-) diff --git a/Readme.md b/Readme.md index 0c3223322..5768a1127 100644 --- a/Readme.md +++ b/Readme.md @@ -133,13 +133,14 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *mixed* **error** - Truthy to indicate an error -#### checkScope (scope, accessToken, callback) -- *mixed* **scope** - - String, array, or object indicating which scope(s) a token must possess +#### authoriseScope (accessToken, scope, callback) - *string* **accessToken** -- *function* **callback (error)** +- *mixed* **scope** +- *function* **callback (error, allowed)** - *mixed* **error** - Truthy to indicate an error + - *boolean* **allowed** + - Indicates whether the scope is allowed #### saveScope (scope, accessToken, callback) - *string* **scope** diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index fd634a5a4..835f5cbc6 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -133,14 +133,13 @@ model.saveScope = function (scope, accessToken, callback) { }); }; -model.checkScope = function (scope, accessToken, callback) +model.authoriseScope = function (accessToken, scope, callback) + var allowed = accessToken.scope.indexOf(scope) !== -1; + // You may pass anything from a simple string, as this example illustrates, // to representations including scopes and subscopes such as // { "account": [ "edit" ] } - if(accessToken.scope.indexOf(scope) === -1) { - return callback('Required scope: ' + scope); - } - callback(); + return callback(false, allowed); }; /* diff --git a/lib/authorise.js b/lib/authorise.js index b240eb73a..0a5edfdca 100644 --- a/lib/authorise.js +++ b/lib/authorise.js @@ -26,7 +26,8 @@ module.exports = Authorise; */ var fns = [ getBearerToken, - checkToken + checkToken, + checkScope ]; /** @@ -35,14 +36,16 @@ var fns = [ * @param {Object} config Instance of OAuth object * @param {Object} req * @param {Object} res - * @param {Object} options May indicate required scope(s) + * @param {Object} options * @param {Function} next */ -function Authorise (config, req, scope, next) { +function Authorise (config, req, options, next) { + options = options || {}; + this.config = config; this.model = config.model; this.req = req; - this.scope = scope; + this.options = options; runner(fns, this, next); } @@ -127,13 +130,27 @@ function checkToken (done) { self.req.oauth = { bearerToken: token }; self.req.user = token.user ? token.user : { id: token.userId }; - if(self.scope) { - self.model.checkScope(self.scope, token, function (err) { - if (err) { return done(new error('invalid_scope', err)); } - done(); - }); - } else { - done(); + done(); + }); +} + +/** + * Check scope + * + * @param {Function} done + * @this OAuth + */ + +function checkScope (done) { + if (!this.options.scope) return done(); + + this.model.authoriseScope(this.req.oauth.bearerToken, this.options.scope, function (err, allowed) { + if (err) return done(error('server_error', false, err)); + + if (!allowed) { + return done(error('invalid_scope', 'The requested scope is invalid')); } + + done(); }); } diff --git a/lib/oauth2server.js b/lib/oauth2server.js index 90f206641..0aaee110b 100644 --- a/lib/oauth2server.js +++ b/lib/oauth2server.js @@ -64,11 +64,11 @@ function OAuth2Server (config) { * * @return {Function} middleware */ -OAuth2Server.prototype.authorise = function (scope) { +OAuth2Server.prototype.authorise = function (options) { var self = this; return function (req, res, next) { - return new Authorise(self, req, scope, next); + return new Authorise(self, req, options, next); }; }; diff --git a/test/authorise.js b/test/authorise.js index 0b33f564c..8a40ca9f0 100644 --- a/test/authorise.js +++ b/test/authorise.js @@ -21,28 +21,32 @@ var express = require('express'), var oauth2server = require('../'); -var bootstrap = function (oauthConfig) { - if (oauthConfig === 'mockValid') { - oauthConfig = { - model: { - getAccessToken: function (token, callback) { - token.should.equal('thom'); - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + 20); - callback(false, { expires: expires }); - } - } - }; - } - +var bootstrap = function (model, options, continueAfterResponse) { var app = express(); - app.oauth = oauth2server(oauthConfig || { model: {} }); + + model = model || { + getAccessToken: function (token, callback) { + var expires = new Date(Date.now() * 2); + + callback(false, { expires: expires }); + }, + authoriseScope: function (accessToken, scope, cb) { + cb(false, true); + } + }; + + app.oauth = oauth2server({ + model: model || {}, + continueAfterResponse: continueAfterResponse + }); app.use(bodyParser()); - app.all('/', app.oauth.authorise()); + app.get('/', app.oauth.authorise(options), function (req, res) { + res.send('nightworld'); + }); - app.all('/', function (req, res) { + app.post('/', app.oauth.authorise(options), function (req, res) { res.send('nightworld'); }); @@ -51,10 +55,10 @@ var bootstrap = function (oauthConfig) { return app; }; -describe('Authorise', function() { +describe('Authorise', function () { it('should detect no access token', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .get('/') @@ -62,7 +66,7 @@ describe('Authorise', function() { }); it('should allow valid token as query param', function (done){ - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .get('/?access_token=thom') @@ -71,7 +75,7 @@ describe('Authorise', function() { it('should require application/x-www-form-urlencoded when access token is ' + 'in body', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .post('/') @@ -81,7 +85,7 @@ describe('Authorise', function() { }); it('should not allow GET when access token in body', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .get('/') @@ -91,7 +95,7 @@ describe('Authorise', function() { }); it('should allow valid token in body', function (done){ - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .post('/') @@ -100,8 +104,33 @@ describe('Authorise', function() { .expect(200, /nightworld/, done); }); + it('should allow if scope is valid for the token', function (done) { + var app = bootstrap(null, { scope: 'foobar' }); + + request(app) + .get('/?access_token=thom') + .expect(200, /nightworld/, done); + }); + + it('should not allow if scope is invalid for the token', function (done) { + var app = bootstrap({ + getAccessToken: function (token, callback) { + callback(false, { expires: null }); + }, + authoriseScope: function (accessToken, scope, cb) { + cb(false, false); + } + }, { scope: 'foobar' }); + + app.use(app.oauth.errorHandler()); + + request(app) + .get('/?access_token=thom') + .expect(400, /invalid_scope/, done); + }); + it('should detect malformed header', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .get('/') @@ -110,7 +139,7 @@ describe('Authorise', function() { }); it('should allow valid token in header', function (done){ - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .get('/') @@ -119,7 +148,7 @@ describe('Authorise', function() { }); it('should allow exactly one method (get: query + auth)', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .get('/?access_token=thom') @@ -128,7 +157,7 @@ describe('Authorise', function() { }); it('should allow exactly one method (post: query + body)', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .post('/?access_token=thom') @@ -139,7 +168,7 @@ describe('Authorise', function() { }); it('should allow exactly one method (post: query + empty body)', function (done) { - var app = bootstrap('mockValid'); + var app = bootstrap(); request(app) .post('/?access_token=thom') @@ -151,10 +180,8 @@ describe('Authorise', function() { it('should detect expired token', function (done){ var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - callback(false, { expires: 0 }); // Fake expires - } + getAccessToken: function (token, callback) { + callback(false, { expires: 0 }); // Fake expires } }); @@ -163,23 +190,13 @@ describe('Authorise', function() { .expect(401, /the access token provided has expired/i, done); }); - it('should passthrough with valid, non-expiring token (token = null)', - function (done) { + it('should passthrough with valid, non-expiring token (token = null)', function (done) { var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - token.should.equal('thom'); - callback(false, { expires: null }); - } + getAccessToken: function (token, callback) { + callback(false, { expires: null }); } }, false); - app.get('/', app.oauth.authorise(), function (req, res) { - res.send('nightworld'); - }); - - app.use(app.oauth.errorHandler()); - request(app) .get('/?access_token=thom') .expect(200, /nightworld/, done); @@ -187,24 +204,19 @@ describe('Authorise', function() { it('should expose the user id when setting userId', function (done) { var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + 20); - callback(false, { expires: expires , userId: 1 }); - } + getAccessToken: function (token, callback) { + var expires = new Date(Date.now() * 2); + + callback(false, { expires: expires , userId: 1 }); } - }, false); + }); - app.get('/', app.oauth.authorise(), function (req, res) { + app.get('/', function (req, res) { req.should.have.property('user'); req.user.should.have.property('id'); req.user.id.should.equal(1); - res.send('nightworld'); }); - app.use(app.oauth.errorHandler()); - request(app) .get('/?access_token=thom') .expect(200, /nightworld/, done); @@ -212,29 +224,24 @@ describe('Authorise', function() { it('should expose the user id when setting user object', function (done) { var app = bootstrap({ - model: { - getAccessToken: function (token, callback) { - var expires = new Date(); - expires.setSeconds(expires.getSeconds() + 20); - callback(false, { expires: expires , user: { id: 1, name: 'thom' }}); - } + getAccessToken: function (token, callback) { + var expires = new Date(Date.now() * 2); + + callback(false, { expires: expires, user: { id: 1, name: 'thom' }}); } - }, false); + }); - app.get('/', app.oauth.authorise(), function (req, res) { + app.get('/', function (req, res) { req.should.have.property('user'); req.user.should.have.property('id'); req.user.id.should.equal(1); req.user.should.have.property('name'); req.user.name.should.equal('thom'); - res.send('nightworld'); }); - app.use(app.oauth.errorHandler()); - request(app) .get('/?access_token=thom') .expect(200, /nightworld/, done); }); -}); \ No newline at end of file +}); From 2aeffb0ecae7c89a113110fa826f2634bd26fe2c Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Tue, 10 Feb 2015 18:57:50 +0000 Subject: [PATCH 09/21] Improve scope support in grant middleware --- Readme.md | 8 ++- examples/postgresql/model.js | 4 +- lib/grant.js | 44 +++++++++--- test/authCodeGrant.js | 34 +++++++-- test/grant.client_credentials.js | 2 +- test/grant.js | 118 +++++++++++++++++++++++++++++++ 6 files changed, 189 insertions(+), 21 deletions(-) diff --git a/Readme.md b/Readme.md index 5768a1127..550bf349c 100644 --- a/Readme.md +++ b/Readme.md @@ -142,12 +142,14 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *boolean* **allowed** - Indicates whether the scope is allowed -#### saveScope (scope, accessToken, callback) -- *string* **scope** +#### saveScope (accessToken, scope, callback) - *object* **accessToken** -- *function* **callback (error)** +- *string* **scope** +- *function* **callback (error, scope)** - *mixed* **error** - Truthy to indicate an error + - *mixed* **scope** + - The accepted scope, or falsy to indicate an invalid scope. ### Required for `authorization_code` grant type diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index 835f5cbc6..1a5115865 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -38,7 +38,7 @@ model.getAccessToken = function (bearerToken, callback) { clientId: token.client_id, expires: token.expires, userId: token.userId, - scope: token.scope.split(' ') // Assumes a flat, space-separated scope string + scope: token.scope.split(' ') // Assumes a flat, space-delimited scope string }); done(); }); @@ -116,7 +116,7 @@ model.saveRefreshToken = function (refreshToken, clientId, expires, userId, call }); }; -model.saveScope = function (scope, accessToken, callback) { +model.saveScope = function (accessToken, scope, callback) { // Here you will want to validate that what the client is soliciting // makes sense. You might then proceed by storing the validated scope. // In this example, the scope is simply stored as a string in the diff --git a/lib/grant.js b/lib/grant.js index 0704c50ca..25d3b4db7 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -133,6 +133,8 @@ function credsFromBody (req) { * @this OAuth */ function checkClient (done) { + var self = this; + this.model.getClient(this.client.clientId, this.client.clientSecret, function (err, client) { if (err) return done(error('server_error', false, err)); @@ -141,6 +143,8 @@ function checkClient (done) { return done(error('invalid_client', 'Client credentials are invalid')); } + // Expose validated client + self.scope = client.scope; done(); }); } @@ -194,7 +198,8 @@ function useAuthCodeGrant (done) { } self.user = authCode.user || { id: authCode.userId }; - self.scope = authCode.scope || ''; + self.scope = authCode.scope; + if (!self.user.id) { return done(error('server_error', false, 'No user/userId parameter returned from getauthCode')); @@ -226,6 +231,8 @@ function usePasswordGrant (done) { } self.user = user; + self.scope = self.req.body.scope; + done(); }); } @@ -259,7 +266,7 @@ function useRefreshTokenGrant (done) { } self.user = refreshToken.user || { id: refreshToken.userId }; - self.scope = refreshToken.scope || ''; + self.scope = refreshToken.scope; if (self.model.revokeRefreshToken) { return self.model.revokeRefreshToken(token, function (err) { @@ -288,14 +295,16 @@ function useClientCredentialsGrant (done) { } var self = this; - return this.model.getUserFromClient(clientId, clientSecret, - function (err, user) { + return this.model.getUserFromClient(clientId, clientSecret, function (err, user) { if (err) return done(error('server_error', false, err)); + if (!user) { return done(error('invalid_grant', 'Client credentials are invalid')); } self.user = user; + self.scope = self.client.scope; + done(); }); } @@ -458,12 +467,22 @@ function saveRefreshToken (done) { * @this OAuth */ function saveScope (done) { - var scope = this.scope || this.req.body.scope; - var self = this; - this.model.saveScope(scope, self.accessToken, function (err, acceptedScope) { + + // If no scope was given, there is no need to ask the model to save the scope. + // If you need to assign a default scope, then do so from the `getAccessToken` + // model callback. + if (!this.scope) return done(); + + this.model.saveScope(this.accessToken, this.scope, function (err, scope) { if (err) return done(error('server_error', false, err)); - self.scope = acceptedScope; + + if (!scope) { + return done(error('invalid_scope', 'The requested scope is invalid')); + } + + self.scope = scope; + done(); }); } @@ -477,7 +496,6 @@ function saveScope (done) { function sendResponse (done) { var response = { token_type: 'bearer', - scope: this.scope, access_token: this.accessToken }; @@ -485,7 +503,13 @@ function sendResponse (done) { response.expires_in = this.config.accessTokenLifetime; } - if (this.refreshToken) response.refresh_token = this.refreshToken; + if (this.refreshToken) { + response.refresh_token = this.refreshToken; + } + + if (this.scope) { + response.scope = this.scope; + } this.res.jsonp(response); diff --git a/test/authCodeGrant.js b/test/authCodeGrant.js index 1ccecfdf3..8b0cc3b5c 100644 --- a/test/authCodeGrant.js +++ b/test/authCodeGrant.js @@ -211,7 +211,7 @@ describe('AuthCodeGrant', function() { redirectUri: 'http://nightworld.com' }); }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { + saveAuthCode: function (authCode, clientId, expires, user, scope, callback) { should.exist(authCode); authCode.should.have.lengthOf(40); clientId.should.equal('thom'); @@ -230,6 +230,30 @@ describe('AuthCodeGrant', function() { .end(); }); + it('should return a scope', function (done) { + var app = bootstrap({ + getClient: function (clientId, clientSecret, callback) { + callback(false, { + clientId: 'thom', + redirectUri: 'http://nightworld.com' + }); + }, + saveAuthCode: function (authCode, clientId, expires, user, scope, callback) { + scope.should.equal('foobar'); + done(); + } + }, [false, true, false, 'foobar']); + + request(app) + .post('/authorise') + .send({ + response_type: 'code', + client_id: 'thom', + redirect_uri: 'http://nightworld.com' + }) + .end(); + }); + it('should accept valid request and return code using POST', function (done) { var code; @@ -240,7 +264,7 @@ describe('AuthCodeGrant', function() { redirectUri: 'http://nightworld.com' }); }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { + saveAuthCode: function (authCode, clientId, expires, user, scope, callback) { should.exist(authCode); code = authCode; callback(); @@ -270,7 +294,7 @@ describe('AuthCodeGrant', function() { redirectUri: 'http://nightworld.com' }); }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { + saveAuthCode: function (authCode, clientId, expires, user, scope, callback) { should.exist(authCode); code = authCode; callback(); @@ -300,7 +324,7 @@ describe('AuthCodeGrant', function() { redirectUri: 'http://nightworld.com' }); }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { + saveAuthCode: function (authCode, clientId, expires, user, scope, callback) { should.exist(authCode); code = authCode; callback(); @@ -329,7 +353,7 @@ describe('AuthCodeGrant', function() { redirectUri: 'http://nightworld.com' }); }, - saveAuthCode: function (authCode, clientId, expires, user, callback) { + saveAuthCode: function (authCode, clientId, expires, user, scope, callback) { callback(); } }, [false, true], true); diff --git a/test/grant.client_credentials.js b/test/grant.client_credentials.js index 730946167..43994eed0 100644 --- a/test/grant.client_credentials.js +++ b/test/grant.client_credentials.js @@ -70,4 +70,4 @@ describe('Granting with client_credentials grant type', function () { .expect(400, /client credentials are invalid/i, done); }); -}); \ No newline at end of file +}); diff --git a/test/grant.js b/test/grant.js index 2d5820de0..a78322592 100644 --- a/test/grant.js +++ b/test/grant.js @@ -469,6 +469,53 @@ describe('Grant', function() { }); + it('should return an oauth compatible response with scope', function (done) { + var app = bootstrap({ + model: { + getClient: function (id, secret, callback) { + callback(false, { client_id: 'thom' }); + }, + grantTypeAllowed: function (clientId, grantType, callback) { + callback(false, true); + }, + getUser: function (uname, pword, callback) { + callback(false, { id: 1 }); + }, + saveAccessToken: function (token, clientId, expires, user, cb) { + cb(); + }, + saveRefreshToken: function (token, clientId, expires, user, cb) { + cb(); + }, + saveScope: function (accessToken, scope, cb) { + cb(false, 'foobar'); + } + }, + grants: ['password', 'refresh_token'] + }); + + request(app) + .post('/oauth/token') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld', username: 'thomseddon', password: 'nightworld', scope: 'foobar' }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + + res.body.should.have.keys(['access_token', 'token_type', 'expires_in', + 'refresh_token', 'scope']); + res.body.access_token.should.be.instanceOf(String); + res.body.access_token.should.have.length(40); + res.body.expires_in.should.equal(3600); + res.body.refresh_token.should.be.instanceOf(String); + res.body.refresh_token.should.have.length(40); + res.body.scope.should.equal('foobar'); + res.body.token_type.should.equal('bearer'); + + done(); + }); + }); + it('should exclude expires_in if accessTokenLifetime = null', function (done) { var app = bootstrap({ model: { @@ -554,4 +601,75 @@ describe('Grant', function() { }); + describe('saving scope', function () { + it('should not allow invalid scopes', function (done) { + var app = bootstrap({ + model: { + getClient: function (id, secret, callback) { + callback(false, { client_id: 'thom' }); + }, + grantTypeAllowed: function (clientId, grantType, callback) { + callback(false, true); + }, + getUser: function (uname, pword, callback) { + callback(false, { id: 1 }); + }, + saveAccessToken: function (token, clientId, expires, user, cb) { + token.should.be.instanceOf(String); + token.should.have.length(40); + clientId.should.equal('thom'); + user.id.should.equal(1); + (+expires).should.be.within(10, (+new Date()) + 3600000); + cb(); + }, + saveScope: function(accessToken, scope, cb) { + cb(false, false); + } + }, + grants: ['password'] + }); + + request(app) + .post('/oauth/token') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld', username: 'thomseddon', password: 'nightworld', scope: 'foo bar' }) + .expect(400, /invalid_scope/, done); + + }); + + it('should allow valid scopes', function (done) { + var app = bootstrap({ + model: { + getClient: function (id, secret, callback) { + callback(false, { client_id: 'thom' }); + }, + grantTypeAllowed: function (clientId, grantType, callback) { + callback(false, true); + }, + getUser: function (uname, pword, callback) { + callback(false, { id: 1 }); + }, + saveAccessToken: function (token, clientId, expires, user, cb) { + token.should.be.instanceOf(String); + token.should.have.length(40); + clientId.should.equal('thom'); + user.id.should.equal(1); + (+expires).should.be.within(10, (+new Date()) + 3600000); + cb(); + }, + saveScope: function(accessToken, scope, cb) { + cb(false, 'foo bar'); + } + }, + grants: ['password'] + }); + + request(app) + .post('/oauth/token') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld', username: 'thomseddon', password: 'nightworld', scope: 'foo bar' }) + .expect(200, /foo bar/, done); + }); + }); + }); From 97e17cb81d6f88492c8983890021362da6df70a3 Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Wed, 11 Mar 2015 00:15:22 +0000 Subject: [PATCH 10/21] Improve scope middleware and add tests --- lib/scope.js | 19 +++++++----- test/scope.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 test/scope.js diff --git a/lib/scope.js b/lib/scope.js index e0db7f60b..6cc580f1d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -34,14 +34,13 @@ var fns = [ * @param {Object} config Instance of OAuth object * @param {Object} req * @param {Function} next - * @param {Mixed} requiredScope String or list of scope keys required - * to access the resource + * @param {Mixed} scope String or list of required scopes */ -function Scope (config, req, next, requiredScope) { +function Scope (config, req, next, scope) { this.config = config; this.model = config.model; this.req = req; - this.requiredScope = requiredScope; + this.scope = scope; runner(fns, this, next); } @@ -57,9 +56,15 @@ function Scope (config, req, next, requiredScope) { * */ function checkScope (done) { - this.model.checkScope(this.requiredScope, - this.req.oauth.bearerToken, function (err) { - if (err) { return done(new error('invalid_scope', err)); } + if (!this.req.oauth) return done(error('invalid_request', 'Request not authenticated')); + + this.model.checkScope(this.req.oauth.bearerToken, this.scope, function (err, allowed) { + if (err) return done(error('server_error', false, err)); + + if (!allowed) { + return done(error('invalid_scope', 'The requested scope is invalid')); + } + done(); }); } diff --git a/test/scope.js b/test/scope.js new file mode 100644 index 000000000..0219b8806 --- /dev/null +++ b/test/scope.js @@ -0,0 +1,83 @@ +/** + * Copyright 2013-present NightWorld. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var express = require('express'), + bodyParser = require('body-parser'), + request = require('supertest'), + should = require('should'); + +var oauth2server = require('../'); + +var bootstrap = function (options) { + var app = express(); + + app.oauth = oauth2server({ + model: { + getAccessToken: function (token, callback) { + var expires = new Date(Date.now() * 2); + + callback(false, { expires: expires }); + }, + checkScope: function (accessToken, scope, cb) { + cb(false, 'my-scope' === scope); + } + } + }); + + app.use(bodyParser()); + + app.get('/', app.oauth.authorise(), app.oauth.scope(options), function (req, res) { + res.send('nightworld'); + }); + + app.use(app.oauth.errorHandler()); + + return app; +}; + +describe('Scope', function () { + + it('should not allow if not authorized', function (done) { + var app = bootstrap('foobar'); + + app.get('/unauthorised', app.oauth.scope('foobar'), function (req, res) { + res.send('nightworld'); + }); + + app.use(app.oauth.errorHandler()); + + request(app) + .get('/unauthorised') + .expect(400, /invalid_request/, done); + }); + + it('should not allow if scope is invalid', function (done) { + var app = bootstrap('foobar'); + + request(app) + .get('/?access_token=thom') + .expect(400, /invalid_scope/, done); + }); + + it('should allow if scope is valid', function (done) { + var app = bootstrap('my-scope'); + + request(app) + .get('/?access_token=thom') + .expect(200, /nightworld/, done); + }); + +}); From 0726291853329b98d3d76322c7b6e788eae3356a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Thu, 12 Mar 2015 21:10:02 -0700 Subject: [PATCH 11/21] Scope improvements The grant middleware now delegates scope string validation to the model, allowing for proper error handling in case a client performs an invalid request. Valid scope strings are thereafter passed to the model in the `saveAccessToken` call; `saveScope` has been removed. The `authoriseScope` method has been modified to accept a mixed `invalid` argument, which should be falsey to indicate that the access token possesses the required scope, and truthy (boolean or string) if not. If a string is provided, it is used as error description for the `invalid_scope` error. The error class has been extended with a default "Invalid scope" message for boolean true cases. --- Readme.md | 19 ++++++++++-------- lib/authorise.js | 8 +++----- lib/error.js | 3 +++ lib/grant.js | 50 ++++++++++++++++++++---------------------------- lib/scope.js | 7 ++----- 5 files changed, 40 insertions(+), 47 deletions(-) diff --git a/Readme.md b/Readme.md index 550bf349c..0f94cf254 100644 --- a/Readme.md +++ b/Readme.md @@ -124,11 +124,12 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *boolean* **allowed** - Indicates whether the grantType is allowed for this clientId -#### saveAccessToken (accessToken, clientId, expires, user, callback) +#### saveAccessToken (accessToken, clientId, expires, user, scope, callback) - *string* **accessToken** - *string* **clientId** - *date* **expires** - *object* **user** +- *string* **scope** - *function* **callback (error)** - *mixed* **error** - Truthy to indicate an error @@ -139,17 +140,19 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *function* **callback (error, allowed)** - *mixed* **error** - Truthy to indicate an error - - *boolean* **allowed** - - Indicates whether the scope is allowed + - *boolean|string* **invalid** + - Falsey to indicate token possesses required scope; truthy (boolean or string) as invalid scope error message -#### saveScope (accessToken, scope, callback) -- *object* **accessToken** +### validateScope (scope, clientId, callback) - *string* **scope** -- *function* **callback (error, scope)** +- *string* **clientId** +- *function* **callback (error, validScope, invalid)** - *mixed* **error** - Truthy to indicate an error - - *mixed* **scope** - - The accepted scope, or falsy to indicate an invalid scope. + - *mixed* **validScope** + - Validated/sanitized scope string + - *boolean|string* **invalid** + - Falsey to indicate solicited scope was granted; truthy (boolean or string) as invalid scope error message ### Required for `authorization_code` grant type diff --git a/lib/authorise.js b/lib/authorise.js index 0a5edfdca..e9d32414c 100644 --- a/lib/authorise.js +++ b/lib/authorise.js @@ -144,12 +144,10 @@ function checkToken (done) { function checkScope (done) { if (!this.options.scope) return done(); - this.model.authoriseScope(this.req.oauth.bearerToken, this.options.scope, function (err, allowed) { + this.model.authoriseScope(this.req.oauth.bearerToken, this.options.scope, + function (err, invalid) { if (err) return done(error('server_error', false, err)); - - if (!allowed) { - return done(error('invalid_scope', 'The requested scope is invalid')); - } + if (invalid) return done(error('invalid_scope', invalid)); done(); }); diff --git a/lib/error.js b/lib/error.js index 36e22aa0e..072f033e7 100644 --- a/lib/error.js +++ b/lib/error.js @@ -47,6 +47,9 @@ function OAuth2Error (error, description, err) { }; /* falls through */ case 'invalid_scope': + if (typeof this.message === 'boolean') { + this.message = 'Invalid scope'; + } case 'invalid_grant': case 'invalid_request': this.code = 400; diff --git a/lib/grant.js b/lib/grant.js index 25d3b4db7..a5391fecc 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -31,12 +31,12 @@ var fns = [ checkClient, checkGrantTypeAllowed, checkGrantType, + checkScope, exposeParams, generateAccessToken, saveAccessToken, generateRefreshToken, saveRefreshToken, - saveScope, sendResponse ]; @@ -355,6 +355,25 @@ function checkGrantTypeAllowed (done) { }); } +/** + * Validate the scope request + * + * @param {Function} done + * @this OAuth + */ +function checkScope (done) { + var self = this; + this.model.validateScope(this.scope, this.client.clientId, + function(err, scope, invalid) { + if (err) return done(error('server_error', false, err)); + if (invalid) return done(error('invalid_scope', invalid)); + + self.scope = scope; + + done(); + }); +} + /** * Expose user and client params * @@ -408,7 +427,7 @@ function saveAccessToken (done) { } this.model.saveAccessToken(accessToken, this.client.clientId, expires, - this.user, function (err) { + this.user, this.scope, function (err) { if (err) return done(error('server_error', false, err)); done(); }); @@ -460,33 +479,6 @@ function saveRefreshToken (done) { }); } -/** - * Pass the scope string to the model for saving - * - * @param {Function} done - * @this OAuth - */ -function saveScope (done) { - var self = this; - - // If no scope was given, there is no need to ask the model to save the scope. - // If you need to assign a default scope, then do so from the `getAccessToken` - // model callback. - if (!this.scope) return done(); - - this.model.saveScope(this.accessToken, this.scope, function (err, scope) { - if (err) return done(error('server_error', false, err)); - - if (!scope) { - return done(error('invalid_scope', 'The requested scope is invalid')); - } - - self.scope = scope; - - done(); - }); -} - /** * Sends the resulting token(s) and related information to the client * diff --git a/lib/scope.js b/lib/scope.js index 6cc580f1d..e83a43cc4 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -58,12 +58,9 @@ function Scope (config, req, next, scope) { function checkScope (done) { if (!this.req.oauth) return done(error('invalid_request', 'Request not authenticated')); - this.model.checkScope(this.req.oauth.bearerToken, this.scope, function (err, allowed) { + this.model.authoriseScope(this.req.oauth.bearerToken, this.scope, function (err, invalid) { if (err) return done(error('server_error', false, err)); - - if (!allowed) { - return done(error('invalid_scope', 'The requested scope is invalid')); - } + if (invalid) return done(error('invalid_scope', invalid)); done(); }); From 92d4ea762e0c6c5f882a4cc727826d3c05d9a054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Thu, 12 Mar 2015 21:13:00 -0700 Subject: [PATCH 12/21] Updated tests The tests were updated for the modified scope behaviours. --- test/authorise.js | 4 +- test/grant.authorization_code.js | 8 +++- test/grant.extended.js | 3 ++ test/grant.js | 69 +++++++++++++++++++++++--------- test/grant.refresh_token.js | 15 +++++-- test/scope.js | 4 +- 6 files changed, 74 insertions(+), 29 deletions(-) diff --git a/test/authorise.js b/test/authorise.js index 8a40ca9f0..33b0906fd 100644 --- a/test/authorise.js +++ b/test/authorise.js @@ -31,7 +31,7 @@ var bootstrap = function (model, options, continueAfterResponse) { callback(false, { expires: expires }); }, authoriseScope: function (accessToken, scope, cb) { - cb(false, true); + cb(false, false); } }; @@ -118,7 +118,7 @@ describe('Authorise', function () { callback(false, { expires: null }); }, authoriseScope: function (accessToken, scope, cb) { - cb(false, false); + cb(false, true); } }, { scope: 'foobar' }); diff --git a/test/grant.authorization_code.js b/test/grant.authorization_code.js index bfd6f853d..2f034c394 100644 --- a/test/grant.authorization_code.js +++ b/test/grant.authorization_code.js @@ -197,10 +197,11 @@ describe('Granting with authorization_code grant type', function () { callback(false, { clientId: 'thom', expires: new Date(), - userId: '123' + userId: '123', + scope: 'foobar' }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (data, cb) { @@ -208,6 +209,9 @@ describe('Granting with authorization_code grant type', function () { }, expireRefreshToken: function (refreshToken, callback) { callback(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, 'foobar', false); } }, grants: ['authorization_code'] diff --git a/test/grant.extended.js b/test/grant.extended.js index 6c6191118..ea1e9b26d 100644 --- a/test/grant.extended.js +++ b/test/grant.extended.js @@ -131,6 +131,9 @@ describe('Granting with extended grant type', function () { }, saveAccessToken: function () { done(); // That's enough + }, + validateScope: function (scope, clientId, cb) { + cb(false, '', false); } }, grants: ['http://custom.com'] diff --git a/test/grant.js b/test/grant.js index a78322592..700e51010 100644 --- a/test/grant.js +++ b/test/grant.js @@ -43,7 +43,8 @@ var validBody = { client_id: 'thom', client_secret: 'nightworld', username: 'thomseddon', - password: 'nightworld' + password: 'nightworld', + scope: 'foobar' }; describe('Grant', function() { @@ -236,9 +237,12 @@ describe('Grant', function() { generateToken: function (type, req, callback) { callback(false, 'thommy'); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { token.should.equal('thommy'); cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password'] @@ -270,9 +274,12 @@ describe('Grant', function() { req.user.id.should.equal(1); callback(false, 'thommy'); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { token.should.equal('thommy'); cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password'] @@ -301,8 +308,11 @@ describe('Grant', function() { generateToken: function (type, req, callback) { callback(false, { accessToken: 'thommy' }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(new Error('Should not be saving')); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password'] @@ -330,13 +340,17 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { token.should.be.instanceOf(String); token.should.have.length(40); clientId.should.equal('thom'); user.id.should.equal(1); + scope.should.equal('foobar'); (+expires).should.be.within(10, (+new Date()) + 3600000); cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, 'foobar', false); } }, grants: ['password'] @@ -362,7 +376,7 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { @@ -372,6 +386,9 @@ describe('Grant', function() { user.id.should.equal(1); (+expires).should.be.within(10, (+new Date()) + 1209600000); cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password', 'refresh_token'] @@ -399,8 +416,11 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password'] @@ -437,11 +457,14 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password', 'refresh_token'] @@ -481,14 +504,14 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { cb(); }, - saveScope: function (accessToken, scope, cb) { - cb(false, 'foobar'); + validateScope: function (scope, clientId, cb) { + cb(null, 'foobar', false); } }, grants: ['password', 'refresh_token'] @@ -528,13 +551,16 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { should.strictEqual(null, expires); cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { should.strictEqual(null, expires); cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password', 'refresh_token'], @@ -562,7 +588,7 @@ describe('Grant', function() { }); - it('should continue after success response if continueAfterResponse1 = true', function (done) { + it('should continue after success response if continueAfterResponse = true', function (done) { var app = bootstrap({ model: { getClient: function (id, secret, callback) { @@ -574,8 +600,11 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); + }, + validateScope: function (scope, clientId, cb) { + cb(null, '', false); } }, grants: ['password'], @@ -614,7 +643,7 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { token.should.be.instanceOf(String); token.should.have.length(40); clientId.should.equal('thom'); @@ -622,8 +651,8 @@ describe('Grant', function() { (+expires).should.be.within(10, (+new Date()) + 3600000); cb(); }, - saveScope: function(accessToken, scope, cb) { - cb(false, false); + validateScope: function(scope, clientId, cb) { + cb(false, false, true); } }, grants: ['password'] @@ -649,7 +678,7 @@ describe('Grant', function() { getUser: function (uname, pword, callback) { callback(false, { id: 1 }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { token.should.be.instanceOf(String); token.should.have.length(40); clientId.should.equal('thom'); @@ -657,8 +686,8 @@ describe('Grant', function() { (+expires).should.be.within(10, (+new Date()) + 3600000); cb(); }, - saveScope: function(accessToken, scope, cb) { - cb(false, 'foo bar'); + validateScope: function(scope, clientId, cb) { + cb(false, 'foo bar', false); } }, grants: ['password'] diff --git a/test/grant.refresh_token.js b/test/grant.refresh_token.js index 804cff8ef..53ee69461 100644 --- a/test/grant.refresh_token.js +++ b/test/grant.refresh_token.js @@ -171,7 +171,7 @@ describe('Granting with refresh_token grant type', function () { userId: '123' }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { @@ -179,6 +179,9 @@ describe('Granting with refresh_token grant type', function () { }, expireRefreshToken: function (refreshToken, callback) { callback(); + }, + validateScope: function (scope, clientId, cb) { + cb(false, '', false); } }, grants: ['password', 'refresh_token'] @@ -216,7 +219,7 @@ describe('Granting with refresh_token grant type', function () { } }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { @@ -224,6 +227,9 @@ describe('Granting with refresh_token grant type', function () { }, expireRefreshToken: function (refreshToken, callback) { callback(); + }, + validateScope: function (scope, clientId, cb) { + cb(false, '', false); } }, grants: ['password', 'refresh_token'] @@ -258,7 +264,7 @@ describe('Granting with refresh_token grant type', function () { userId: '123' }); }, - saveAccessToken: function (token, clientId, expires, user, cb) { + saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, saveRefreshToken: function (token, clientId, expires, user, cb) { @@ -266,6 +272,9 @@ describe('Granting with refresh_token grant type', function () { }, expireRefreshToken: function (refreshToken, callback) { callback(); + }, + validateScope: function (scope, clientId, cb) { + cb(false, '', false); } }, grants: ['password', 'refresh_token'] diff --git a/test/scope.js b/test/scope.js index 0219b8806..e21df5c18 100644 --- a/test/scope.js +++ b/test/scope.js @@ -31,8 +31,8 @@ var bootstrap = function (options) { callback(false, { expires: expires }); }, - checkScope: function (accessToken, scope, cb) { - cb(false, 'my-scope' === scope); + authoriseScope: function (accessToken, scope, cb) { + cb(false, 'my-scope' !== scope); } } }); From bb1206c8a1a85a1fe22e4db4c2ef32259af85cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Thu, 19 Mar 2015 00:25:26 -0700 Subject: [PATCH 13/21] Updated postgresql reference implementation This commit removes remaining references to `saveScope`, and brings the postgresql example up to date by adding the `validateScope` method and storing the scope string through `saveAccessToken`. --- Readme.md | 3 +- examples/postgresql/model.js | 60 ++++++++++++++++------------------ examples/postgresql/schema.sql | 1 + 3 files changed, 31 insertions(+), 33 deletions(-) diff --git a/Readme.md b/Readme.md index 0f94cf254..a073e01e4 100644 --- a/Readme.md +++ b/Readme.md @@ -310,9 +310,8 @@ This will then call the following on your model (in this order): - getClient (clientId, clientSecret, callback) - grantTypeAllowed (clientId, grantType, callback) - getUser (username, password, callback) - - saveAccessToken (accessToken, clientId, expires, user, callback) + - saveAccessToken (accessToken, clientId, expires, user, scope, callback) - saveRefreshToken (refreshToken, clientId, expires, user, callback) **(if using)** - - saveScope (scope, accessToken, callback) Provided there weren't any errors, this will return the following (excluding the `refresh_token` if you've not enabled the refresh_token grant type): diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index 1a5115865..514d269a0 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -70,13 +70,10 @@ model.getClient = function (clientId, clientSecret, callback) { model.getRefreshToken = function (bearerToken, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - // Note: To avoid replicating the scope string in both token tables, the old - // access token's scope string must be retrieved and passed along from here. - client.query('SELECT rt.refresh_token, rt.client_id, rt.expires, rt.user_id, at.scope FROM ' + - 'oauth_refresh_tokens AS rt, oauth_access_tokens AS at WHERE rt.user_id = ' + - 'at.user_id AND rt.client_id = at.client_id AND rt.refresh_token = $', - [bearerToken], function (err, result) { + client.query('SELECT refresh_token, scope, client_id, expires, user_id FROM oauth_refresh_tokens ' + + 'WHERE refresh_token = $1', [bearerToken], function(err, result) { callback(err, result.rowCount ? result.rows[0] : false); + done(); }); }); }; @@ -92,11 +89,11 @@ model.grantTypeAllowed = function (clientId, grantType, callback) { callback(false, true); }; -model.saveAccessToken = function (accessToken, clientId, expires, userId, callback) { +model.saveAccessToken = function (accessToken, clientId, expires, userId, scope, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - client.query('INSERT INTO oauth_access_tokens(access_token, client_id, user_id, expires) ' + - 'VALUES ($1, $2, $3, $4)', [accessToken, clientId, userId, expires], + client.query('INSERT INTO oauth_access_tokens(access_token, client_id, user_id, scope, expires) ' + + 'VALUES ($1, $2, $3, $4, $5)', [accessToken, clientId, userId, scope, expires], function (err, result) { callback(err); done(); @@ -107,8 +104,15 @@ model.saveAccessToken = function (accessToken, clientId, expires, userId, callba model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, user_id, ' + - 'expires) VALUES ($1, $2, $3, $4)', [refreshToken, clientId, userId, expires], + // Retrieve the scope string from the access token entry + client.query('SELECT scope FROM oauth_access_tokens WHERE client_id = $1 AND user_id = $2', + [clientId, userId], function (err, result) { + if (err) return callback(err); + if (!result.rowCount) return callback('Could not retrieve access token scope string'); + + client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, ' + + 'user_id, scope, expires) VALUES ($1, $2, $3, $4, $5)', + [refreshToken, clientId, userId, result.rows[0].scope, expires], function (err, result) { callback(err); done(); @@ -116,30 +120,24 @@ model.saveRefreshToken = function (refreshToken, clientId, expires, userId, call }); }; -model.saveScope = function (accessToken, scope, callback) { - // Here you will want to validate that what the client is soliciting - // makes sense. You might then proceed by storing the validated scope. - // In this example, the scope is simply stored as a string in the - // oauth_access_tokens table, but you could also handle them as entries - // in a connection table. - var acceptedScope = scope; - - pg.connect(connString, function (err, client, done) { - if (err) return callback(err); - client.query('UPDATE oauth_access_tokens SET scope=$1 WHERE access_token = $2', - [acceptedScope, accessToken], function (err, result) { - callback(err, acceptedScope); - done(); - }); -}; - -model.authoriseScope = function (accessToken, scope, callback) - var allowed = accessToken.scope.indexOf(scope) !== -1; +model.authoriseScope = function (accessToken, scope, callback) { + var hasScope = accessToken.scope.indexOf(scope) !== -1; // You may pass anything from a simple string, as this example illustrates, // to representations including scopes and subscopes such as // { "account": [ "edit" ] } - return callback(false, allowed); + return callback(false, hasScope ? false : 'Missing scope: ' + scope); +}; + +model.validateScope = function (scope, clientId, callback) { + // Sanitize the requested scope string, possibly + // against a client-specific set of valid scope keys + var validKeys = ['readonly', 'edit', 'admin']; + var isValid = scope.split(' ').every(function(key) { + return valid.indexOf(key) !== -1; + }); + + return callback(false, isValid ? false : 'Invalid scope request'); }; /* diff --git a/examples/postgresql/schema.sql b/examples/postgresql/schema.sql index 9fd6c1f61..6946efe68 100644 --- a/examples/postgresql/schema.sql +++ b/examples/postgresql/schema.sql @@ -58,6 +58,7 @@ CREATE TABLE oauth_clients ( CREATE TABLE oauth_refresh_tokens ( refresh_token text NOT NULL, + scope text NOT NULL, client_id text NOT NULL, user_id uuid NOT NULL, expires timestamp without time zone NOT NULL From 036440fbbf3d13b961aed59c6b52cee81d4b6cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Thu, 19 Mar 2015 00:35:36 -0700 Subject: [PATCH 14/21] Added missing callback parameter to `model.validateScope` Added the `validScope` callback parameter for the `validateScope` method. Fixed variable name mismatch in Readme. --- Readme.md | 2 +- examples/postgresql/model.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index a073e01e4..8ba37e10e 100644 --- a/Readme.md +++ b/Readme.md @@ -137,7 +137,7 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ #### authoriseScope (accessToken, scope, callback) - *string* **accessToken** - *mixed* **scope** -- *function* **callback (error, allowed)** +- *function* **callback (error, invalid)** - *mixed* **error** - Truthy to indicate an error - *boolean|string* **invalid** diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index 514d269a0..998a6eab2 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -132,12 +132,13 @@ model.authoriseScope = function (accessToken, scope, callback) { model.validateScope = function (scope, clientId, callback) { // Sanitize the requested scope string, possibly // against a client-specific set of valid scope keys + // You could choose to strip invalid keys, or return an error message var validKeys = ['readonly', 'edit', 'admin']; var isValid = scope.split(' ').every(function(key) { return valid.indexOf(key) !== -1; }); - return callback(false, isValid ? false : 'Invalid scope request'); + return callback(false, scope, isValid ? false : 'Invalid scope request'); }; /* From eef76525b21f20bf60a6f97f05cba01bfa34179f Mon Sep 17 00:00:00 2001 From: Chris Camaratta Date: Mon, 20 Apr 2015 12:19:10 -0400 Subject: [PATCH 15/21] added user-scope filtering support for password grant type --- lib/grant.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/grant.js b/lib/grant.js index a5391fecc..92ff7adf4 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -143,8 +143,10 @@ function checkClient (done) { return done(error('invalid_client', 'Client credentials are invalid')); } - // Expose validated client - self.scope = client.scope; + // Expose validated client scope information + self.client.validScopes = client.validScopes; + self.client.defaultScope = client.defaultScope; + done(); }); } @@ -363,7 +365,7 @@ function checkGrantTypeAllowed (done) { */ function checkScope (done) { var self = this; - this.model.validateScope(this.scope, this.client.clientId, + this.model.validateScope(this.scope, this.client, this.user, function(err, scope, invalid) { if (err) return done(error('server_error', false, err)); if (invalid) return done(error('invalid_scope', invalid)); @@ -384,7 +386,8 @@ function exposeParams (done) { this.req.oauth = this.req.oauth || {}; this.req.oauth.client = { id: this.client.clientId, - secret: this.client.clientSecret + secret: this.client.clientSecret, + scope: this.client.scope }; this.req.user = this.user; From ed89b4e525edeeb845dc76060fbadf4d9da9b2c2 Mon Sep 17 00:00:00 2001 From: Chris Camaratta Date: Tue, 21 Apr 2015 11:11:56 -0700 Subject: [PATCH 16/21] Prevented scope from being overwritten Updated PostGreSql example --- examples/postgresql/model.js | 51 +++++++++++++++++++++------------- examples/postgresql/schema.sql | 7 +++-- lib/grant.js | 3 +- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index 998a6eab2..ffd76c640 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -49,7 +49,7 @@ model.getClient = function (clientId, clientSecret, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - client.query('SELECT client_id, client_secret, redirect_uri FROM oauth_clients WHERE ' + + client.query('SELECT client_id, client_secret, redirect_uri, valid_scopes, default_scope FROM oauth_clients WHERE ' + 'client_id = $1', [clientId], function (err, result) { if (err || !result.rowCount) return callback(err); @@ -60,7 +60,9 @@ model.getClient = function (clientId, clientSecret, callback) { // This object will be exposed in req.oauth.client callback(null, { clientId: client.client_id, - clientSecret: client.client_secret + clientSecret: client.client_secret, + validScopes: client.valid_scopes, + defaultScope: client.default_scope }); done(); }); @@ -107,16 +109,17 @@ model.saveRefreshToken = function (refreshToken, clientId, expires, userId, call // Retrieve the scope string from the access token entry client.query('SELECT scope FROM oauth_access_tokens WHERE client_id = $1 AND user_id = $2', [clientId, userId], function (err, result) { - if (err) return callback(err); - if (!result.rowCount) return callback('Could not retrieve access token scope string'); - - client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, ' + - 'user_id, scope, expires) VALUES ($1, $2, $3, $4, $5)', - [refreshToken, clientId, userId, result.rows[0].scope, expires], - function (err, result) { - callback(err); - done(); - }); + if (err) return callback(err); + if (!result.rowCount) return callback('Could not retrieve access token scope string'); + + client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, ' + + 'user_id, scope, expires) VALUES ($1, $2, $3, $4, $5)', + [refreshToken, clientId, userId, result.rows[0].scope, expires], + function (err, result) { + callback(err); + done(); + }); + }); }); }; @@ -129,14 +132,22 @@ model.authoriseScope = function (accessToken, scope, callback) { return callback(false, hasScope ? false : 'Missing scope: ' + scope); }; -model.validateScope = function (scope, clientId, callback) { - // Sanitize the requested scope string, possibly - // against a client-specific set of valid scope keys +model.validateScope = function (scope, client, user, callback) { + // Sanitize the requested scope string against a client-specific set of valid scope keys // You could choose to strip invalid keys, or return an error message - var validKeys = ['readonly', 'edit', 'admin']; - var isValid = scope.split(' ').every(function(key) { - return valid.indexOf(key) !== -1; - }); + scope = scope || client.defaultScope || false; + var validScopes = client.validScopes; + var isValid = !scope || scope.split(' ').every(function(key) { + return validScopes.indexOf(key) !== -1; + }); + + if (user.allowedScopes) { + var userAllowedScopes = user.allowedScopes.split(' '); + var scopes = validScopes.filter(function(key) { + return userAllowedScopes.indexOf(key) !== -1; + }); + scope = scopes.join(' '); + } return callback(false, scope, isValid ? false : 'Invalid scope request'); }; @@ -147,7 +158,7 @@ model.validateScope = function (scope, clientId, callback) { model.getUser = function (username, password, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - client.query('SELECT id FROM users WHERE username = $1 AND password = $2', [username, + client.query('SELECT id, allowed_scopes AS allowedScopes FROM users WHERE username = $1 AND password = $2', [username, password], function (err, result) { callback(err, result.rowCount ? result.rows[0] : false); done(); diff --git a/examples/postgresql/schema.sql b/examples/postgresql/schema.sql index 6946efe68..39e3fcaef 100644 --- a/examples/postgresql/schema.sql +++ b/examples/postgresql/schema.sql @@ -48,7 +48,9 @@ CREATE TABLE oauth_access_tokens ( CREATE TABLE oauth_clients ( client_id text NOT NULL, client_secret text NOT NULL, - redirect_uri text NOT NULL + redirect_uri text NOT NULL, + valid_scopes text NULL, + default_scope text NULL ); @@ -72,7 +74,8 @@ CREATE TABLE oauth_refresh_tokens ( CREATE TABLE users ( id uuid NOT NULL, username text NOT NULL, - password text NOT NULL + password text NOT NULL, + allowed_scopes text NULL ); diff --git a/lib/grant.js b/lib/grant.js index 92ff7adf4..61bf85c28 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -386,8 +386,7 @@ function exposeParams (done) { this.req.oauth = this.req.oauth || {}; this.req.oauth.client = { id: this.client.clientId, - secret: this.client.clientSecret, - scope: this.client.scope + secret: this.client.clientSecret }; this.req.user = this.user; From 715f4f7eb8e7d44098c032534dfd883c610a758a Mon Sep 17 00:00:00 2001 From: Chris Camaratta Date: Tue, 21 Apr 2015 11:17:43 -0700 Subject: [PATCH 17/21] Updated tests with proposed `validateScope` signature --- test/grant.authorization_code.js | 2 +- test/grant.extended.js | 2 +- test/grant.js | 24 ++++++++++++------------ test/grant.refresh_token.js | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/grant.authorization_code.js b/test/grant.authorization_code.js index 2f034c394..101c6bfd6 100644 --- a/test/grant.authorization_code.js +++ b/test/grant.authorization_code.js @@ -210,7 +210,7 @@ describe('Granting with authorization_code grant type', function () { expireRefreshToken: function (refreshToken, callback) { callback(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, 'foobar', false); } }, diff --git a/test/grant.extended.js b/test/grant.extended.js index ea1e9b26d..6c585beeb 100644 --- a/test/grant.extended.js +++ b/test/grant.extended.js @@ -132,7 +132,7 @@ describe('Granting with extended grant type', function () { saveAccessToken: function () { done(); // That's enough }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(false, '', false); } }, diff --git a/test/grant.js b/test/grant.js index 700e51010..7030700cc 100644 --- a/test/grant.js +++ b/test/grant.js @@ -241,7 +241,7 @@ describe('Grant', function() { token.should.equal('thommy'); cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -278,7 +278,7 @@ describe('Grant', function() { token.should.equal('thommy'); cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -311,7 +311,7 @@ describe('Grant', function() { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(new Error('Should not be saving')); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -349,7 +349,7 @@ describe('Grant', function() { (+expires).should.be.within(10, (+new Date()) + 3600000); cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, 'foobar', false); } }, @@ -387,7 +387,7 @@ describe('Grant', function() { (+expires).should.be.within(10, (+new Date()) + 1209600000); cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -419,7 +419,7 @@ describe('Grant', function() { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -463,7 +463,7 @@ describe('Grant', function() { saveRefreshToken: function (token, clientId, expires, user, cb) { cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -510,7 +510,7 @@ describe('Grant', function() { saveRefreshToken: function (token, clientId, expires, user, cb) { cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, 'foobar', false); } }, @@ -559,7 +559,7 @@ describe('Grant', function() { should.strictEqual(null, expires); cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -603,7 +603,7 @@ describe('Grant', function() { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(null, '', false); } }, @@ -651,7 +651,7 @@ describe('Grant', function() { (+expires).should.be.within(10, (+new Date()) + 3600000); cb(); }, - validateScope: function(scope, clientId, cb) { + validateScope: function(scope, client, user, cb) { cb(false, false, true); } }, @@ -686,7 +686,7 @@ describe('Grant', function() { (+expires).should.be.within(10, (+new Date()) + 3600000); cb(); }, - validateScope: function(scope, clientId, cb) { + validateScope: function(scope, client, user, cb) { cb(false, 'foo bar', false); } }, diff --git a/test/grant.refresh_token.js b/test/grant.refresh_token.js index 53ee69461..11d8f05a4 100644 --- a/test/grant.refresh_token.js +++ b/test/grant.refresh_token.js @@ -180,7 +180,7 @@ describe('Granting with refresh_token grant type', function () { expireRefreshToken: function (refreshToken, callback) { callback(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(false, '', false); } }, @@ -228,7 +228,7 @@ describe('Granting with refresh_token grant type', function () { expireRefreshToken: function (refreshToken, callback) { callback(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(false, '', false); } }, @@ -273,7 +273,7 @@ describe('Granting with refresh_token grant type', function () { expireRefreshToken: function (refreshToken, callback) { callback(); }, - validateScope: function (scope, clientId, cb) { + validateScope: function (scope, client, user, cb) { cb(false, '', false); } }, From 0f4c59e397481a847fa4c1277dde3e15c9b1e5eb Mon Sep 17 00:00:00 2001 From: Chris Camaratta Date: Tue, 21 Apr 2015 11:45:35 -0700 Subject: [PATCH 18/21] updated doc. Made example more accurate. --- Readme.md | 5 +++-- examples/postgresql/model.js | 16 +++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Readme.md b/Readme.md index 8ba37e10e..f9a037010 100644 --- a/Readme.md +++ b/Readme.md @@ -143,9 +143,10 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ - *boolean|string* **invalid** - Falsey to indicate token possesses required scope; truthy (boolean or string) as invalid scope error message -### validateScope (scope, clientId, callback) +### validateScope (scope, client, user, callback) - *string* **scope** -- *string* **clientId** +- *object* **client** +- *object* **user** - *function* **callback (error, validScope, invalid)** - *mixed* **error** - Truthy to indicate an error diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index ffd76c640..ba14f495e 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -134,22 +134,24 @@ model.authoriseScope = function (accessToken, scope, callback) { model.validateScope = function (scope, client, user, callback) { // Sanitize the requested scope string against a client-specific set of valid scope keys + // and the scopes the user actually is allowed to use (if any). // You could choose to strip invalid keys, or return an error message - scope = scope || client.defaultScope || false; - var validScopes = client.validScopes; - var isValid = !scope || scope.split(' ').every(function(key) { + var requestedScope = scope || client.defaultScope || ''; + var requestedScopes = requestedScope.split(' '); + var validScopes = client.validScopes.split(' '); + var isValid = !requestedScope || requestedScopes.every(function(key) { return validScopes.indexOf(key) !== -1; }); if (user.allowedScopes) { var userAllowedScopes = user.allowedScopes.split(' '); - var scopes = validScopes.filter(function(key) { - return userAllowedScopes.indexOf(key) !== -1; + var userScopes = validScopes.filter(function(key) { + return (!scope || requestedScopes.indexOf(key) !== -1) && userAllowedScopes.indexOf(key) !== -1; }); - scope = scopes.join(' '); + requestedScope = userScopes.join(' '); } - return callback(false, scope, isValid ? false : 'Invalid scope request'); + return callback(false, requestedScope, isValid ? false : 'Invalid scope request'); }; /* From 7512224e4fb894ab49de8b0d598a1d17a32b1e6a Mon Sep 17 00:00:00 2001 From: Chris Camaratta Date: Wed, 13 May 2015 11:13:04 -0700 Subject: [PATCH 19/21] Pass scope to `model.saveRefreshToken`. --- Readme.md | 3 ++- examples/postgresql/model.js | 22 ++++++++-------------- lib/grant.js | 2 +- test/grant.js | 8 ++++---- test/grant.refresh_token.js | 6 +++--- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/Readme.md b/Readme.md index f9a037010..424a1e947 100644 --- a/Readme.md +++ b/Readme.md @@ -200,11 +200,12 @@ Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/ ### Required for `refresh_token` grant type -#### saveRefreshToken (refreshToken, clientId, expires, user, callback) +#### saveRefreshToken (refreshToken, clientId, expires, user, scope, callback) - *string* **refreshToken** - *string* **clientId** - *date* **expires** - *object* **user** +- *string* **scope** - *function* **callback (error)** - *mixed* **error** - Truthy to indicate an error diff --git a/examples/postgresql/model.js b/examples/postgresql/model.js index ba14f495e..217b2386e 100644 --- a/examples/postgresql/model.js +++ b/examples/postgresql/model.js @@ -103,22 +103,16 @@ model.saveAccessToken = function (accessToken, clientId, expires, userId, scope, }); }; -model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { +model.saveRefreshToken = function (refreshToken, clientId, expires, userId, scope, callback) { pg.connect(connString, function (err, client, done) { if (err) return callback(err); - // Retrieve the scope string from the access token entry - client.query('SELECT scope FROM oauth_access_tokens WHERE client_id = $1 AND user_id = $2', - [clientId, userId], function (err, result) { - if (err) return callback(err); - if (!result.rowCount) return callback('Could not retrieve access token scope string'); - - client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, ' + - 'user_id, scope, expires) VALUES ($1, $2, $3, $4, $5)', - [refreshToken, clientId, userId, result.rows[0].scope, expires], - function (err, result) { - callback(err); - done(); - }); + + client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, ' + + 'user_id, scope, expires) VALUES ($1, $2, $3, $4, $5)', + [refreshToken, clientId, userId, scope, expires], + function (err, result) { + callback(err); + done(); }); }); }; diff --git a/lib/grant.js b/lib/grant.js index 61bf85c28..8c0bb244c 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -475,7 +475,7 @@ function saveRefreshToken (done) { } this.model.saveRefreshToken(refreshToken, this.client.clientId, expires, - this.user, function (err) { + this.user, this.scope, function (err) { if (err) return done(error('server_error', false, err)); done(); }); diff --git a/test/grant.js b/test/grant.js index 7030700cc..ec5ca03ee 100644 --- a/test/grant.js +++ b/test/grant.js @@ -379,7 +379,7 @@ describe('Grant', function() { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, user, scope, cb) { token.should.be.instanceOf(String); token.should.have.length(40); clientId.should.equal('thom'); @@ -460,7 +460,7 @@ describe('Grant', function() { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, user, scope, cb) { cb(); }, validateScope: function (scope, client, user, cb) { @@ -507,7 +507,7 @@ describe('Grant', function() { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, user, scope, cb) { cb(); }, validateScope: function (scope, client, user, cb) { @@ -555,7 +555,7 @@ describe('Grant', function() { should.strictEqual(null, expires); cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, user, scope, cb) { should.strictEqual(null, expires); cb(); }, diff --git a/test/grant.refresh_token.js b/test/grant.refresh_token.js index 11d8f05a4..254e9977e 100644 --- a/test/grant.refresh_token.js +++ b/test/grant.refresh_token.js @@ -174,7 +174,7 @@ describe('Granting with refresh_token grant type', function () { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, user, scope, cb) { cb(); }, expireRefreshToken: function (refreshToken, callback) { @@ -222,7 +222,7 @@ describe('Granting with refresh_token grant type', function () { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, user, scope, cb) { cb(); }, expireRefreshToken: function (refreshToken, callback) { @@ -267,7 +267,7 @@ describe('Granting with refresh_token grant type', function () { saveAccessToken: function (token, clientId, expires, user, scope, cb) { cb(); }, - saveRefreshToken: function (token, clientId, expires, user, cb) { + saveRefreshToken: function (token, clientId, expires, scope, user, cb) { cb(); }, expireRefreshToken: function (refreshToken, callback) { From f04d32b6d2297a00b9f47e0a6962ed39fbd8a935 Mon Sep 17 00:00:00 2001 From: Chris Camaratta Date: Tue, 19 May 2015 16:47:31 -0700 Subject: [PATCH 20/21] Corrected client credentials flow per review --- lib/grant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grant.js b/lib/grant.js index 8c0bb244c..c93eafce8 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -305,7 +305,7 @@ function useClientCredentialsGrant (done) { } self.user = user; - self.scope = self.client.scope; + self.scope = self.req.body.scope || self.client.defaultScope; done(); }); From 4cb4d8a134005a83ce819ed27d8756274f91f025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Fredrik=20Karlstr=C3=B6m?= Date: Tue, 19 May 2015 19:37:16 -0700 Subject: [PATCH 21/21] Replaced tabs with blankspaces --- lib/error.js | 2 +- lib/grant.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/error.js b/lib/error.js index 072f033e7..299b1ebfc 100644 --- a/lib/error.js +++ b/lib/error.js @@ -46,7 +46,7 @@ function OAuth2Error (error, description, err) { 'WWW-Authenticate': 'Basic realm="Service"' }; /* falls through */ - case 'invalid_scope': + case 'invalid_scope': if (typeof this.message === 'boolean') { this.message = 'Invalid scope'; } diff --git a/lib/grant.js b/lib/grant.js index c93eafce8..07e453777 100644 --- a/lib/grant.js +++ b/lib/grant.js @@ -368,9 +368,9 @@ function checkScope (done) { this.model.validateScope(this.scope, this.client, this.user, function(err, scope, invalid) { if (err) return done(error('server_error', false, err)); - if (invalid) return done(error('invalid_scope', invalid)); + if (invalid) return done(error('invalid_scope', invalid)); - self.scope = scope; + self.scope = scope; done(); });