Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8ebe219
Implements scope support
lfk Jan 19, 2015
c199992
Scoping is now implemented using middleware
lfk Jan 23, 2015
ddac89f
The checkScope method now receives the entire bearerToken object
lfk Jan 26, 2015
4e281d6
Updated Readme and postgresql example
lfk Jan 28, 2015
fd69c79
Documentation fixes and checkScope parameter order changed
lfk Jan 29, 2015
01450c3
Added optional scope argument to authorise middleware
lfk Feb 4, 2015
eec2eed
Fix parameter order in postgres model
nunofgs Feb 10, 2015
ceb8fab
Improve scope support in authorise middleware
nunofgs Feb 10, 2015
2aeffb0
Improve scope support in grant middleware
nunofgs Feb 10, 2015
97e17cb
Improve scope middleware and add tests
nunofgs Mar 11, 2015
28b55ac
Merge branch 'seegno-forks-feature-scope' into feature-scope
lfk Mar 12, 2015
0726291
Scope improvements
lfk Mar 13, 2015
92d4ea7
Updated tests
lfk Mar 13, 2015
bb1206c
Updated postgresql reference implementation
lfk Mar 19, 2015
036440f
Added missing callback parameter to `model.validateScope`
lfk Mar 19, 2015
eef7652
added user-scope filtering support for password grant type
ccamarat Apr 20, 2015
ed89b4e
Prevented scope from being overwritten
ccamarat Apr 21, 2015
715f4f7
Updated tests with proposed `validateScope` signature
ccamarat Apr 21, 2015
0f4c59e
updated doc. Made example more accurate.
ccamarat Apr 21, 2015
7512224
Pass scope to `model.saveRefreshToken`.
ccamarat May 13, 2015
f04d32b
Corrected client credentials flow per review
ccamarat May 19, 2015
a073262
Merge branch 'ccamarat-feature/filter-scope' into feature-scope
lfk May 20, 2015
4cb4d8a
Replaced tabs with blankspaces
lfk May 20, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,36 @@ 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

#### authoriseScope (accessToken, scope, callback)
- *string* **accessToken**
- *mixed* **scope**
- *function* **callback (error, invalid)**
- *mixed* **error**
- Truthy to indicate an error
- *boolean|string* **invalid**
- Falsey to indicate token possesses required scope; truthy (boolean or string) as invalid scope error message

### validateScope (scope, client, user, callback)
- *string* **scope**
- *object* **client**
- *object* **user**
- *function* **callback (error, validScope, invalid)**
- *mixed* **error**
- Truthy to indicate an error
- *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

Expand All @@ -151,12 +172,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
Expand All @@ -178,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
Expand Down Expand Up @@ -275,21 +298,21 @@ 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
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)
- 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)**

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):
Expand All @@ -304,7 +327,8 @@ Pragma: no-cache
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bearer",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "readonly"
}
```

Expand Down
6 changes: 6 additions & 0 deletions examples/postgresql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just personal preference but I would prefer removing the newline before function here, to match the rest of the examples.

// 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');
Expand Down
73 changes: 54 additions & 19 deletions examples/postgresql/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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-delimited scope string
});
done();
});
Expand All @@ -48,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);

Expand All @@ -59,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();
});
Expand All @@ -69,12 +72,11 @@ 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);
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();
});
});
});
};

Expand All @@ -89,37 +91,70 @@ 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();
});
});
};

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);
client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, user_id, ' +
'expires) VALUES ($1, $2, $3, $4)', [refreshToken, clientId, userId, expires],

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();
});
callback(err);
done();
});
});
};

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, hasScope ? false : 'Missing scope: ' + scope);
};

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
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 userScopes = validScopes.filter(function(key) {
return (!scope || requestedScopes.indexOf(key) !== -1) && userAllowedScopes.indexOf(key) !== -1;
});
requestedScope = userScopes.join(' ');
}

return callback(false, requestedScope, isValid ? false : 'Invalid scope request');
};

/*
* Required to support password grant type
*/
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();
Expand Down
9 changes: 7 additions & 2 deletions examples/postgresql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,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
);


Expand All @@ -57,6 +60,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
Expand All @@ -70,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
);


Expand Down
6 changes: 4 additions & 2 deletions lib/authCodeGrant.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -146,6 +146,8 @@ function checkUserApproved (done) {
}

self.user = user;
self.scope = scope;

done();
});
}
Expand Down Expand Up @@ -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();
});
Expand Down
30 changes: 27 additions & 3 deletions lib/authorise.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,26 @@ module.exports = Authorise;
*/
var fns = [
getBearerToken,
checkToken
checkToken,
checkScope
];

/**
* Authorise
*
* @param {Object} config Instance of OAuth object
* @param {Object} config Instance of OAuth object
* @param {Object} req
* @param {Object} res
* @param {Object} options
* @param {Function} next
*/
function Authorise (config, req, next) {
function Authorise (config, req, options, next) {
options = options || {};

this.config = config;
this.model = config.model;
this.req = req;
this.options = options;

runner(fns, this, next);
}
Expand Down Expand Up @@ -128,3 +133,22 @@ function checkToken (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, invalid) {
if (err) return done(error('server_error', false, err));
if (invalid) return done(error('invalid_scope', invalid));

done();
});
}
4 changes: 4 additions & 0 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ function OAuth2Error (error, description, err) {
'WWW-Authenticate': 'Basic realm="Service"'
};
/* falls through */
case 'invalid_scope':
if (typeof this.message === 'boolean') {
this.message = 'Invalid scope';
}
case 'invalid_grant':
case 'invalid_request':
this.code = 400;
Expand Down
Loading