├── .gitignore ├── .jshintignore ├── .travis.yml ├── examples ├── dynamodb │ ├── aws.json │ ├── dal.js │ ├── index.js │ ├── Readme.md │ └── model.js ├── memory │ ├── Readme.md │ └── model.js ├── postgresql │ ├── Readme.md │ ├── index.js │ ├── schema.sql │ └── model.js └── mongodb │ ├── Readme.md │ └── model.js ├── lib ├── runner.js ├── token.js ├── authorise.js ├── authCodeGrant.js ├── oauth2server.js └── grant.js ├── package.json ├── test ├── error.js ├── grant.client_credentials.js ├── lockdown.js ├── errorHandler.js ├── grant.password.js ├── grant.extended.js ├── grant.authorization_code.js ├── authorise.js ├── grant.refresh_token.js ├── authCodeGrant.js └── grant.js ├── npm-debug.log ├── Changelog.md ├── LICENSE └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | -------------------------------------------------------------------------------- /examples/dynamodb/aws.json: -------------------------------------------------------------------------------- 1 | { "accessKeyId": "YOUR ACCESS KEY", "secretAccessKey": "YOUR SECRET KEY", "region": "us-east-1" } 2 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = runner; 4 | 5 | /** 6 | * Run through the sequence of functions 7 | * 8 | * @param {Function} next 9 | * @public 10 | */ 11 | function runner (fns, context, next) { 12 | var last = fns.length - 1; 13 | 14 | (function run(pos) { 15 | fns[pos].call(context, function (err) { 16 | if (err || pos === last) return next(err); 17 | run(++pos); 18 | }); 19 | })(0); 20 | } 21 | -------------------------------------------------------------------------------- /examples/memory/Readme.md: -------------------------------------------------------------------------------- 1 | # In-Memory Example 2 | 3 | ## DO NOT USE THIS EXAMPLE IN PRODUCTION 4 | 5 | The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. 6 | 7 | For example: 8 | 9 | ```js 10 | 11 | var memorystore = require('model.js'); 12 | 13 | app.oauth = oauthserver({ 14 | model: memorystore, 15 | grants: ['password','refresh_token'], 16 | debug: true 17 | }); 18 | 19 | ``` 20 | 21 | # Dump 22 | 23 | You can also dump the contents of the memory store (for debugging) like so: 24 | 25 | ```js 26 | 27 | memorystore.dump(); 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /examples/postgresql/Readme.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL Example 2 | 3 | See schema.sql for the tables referred to in this example 4 | 5 | The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. 6 | 7 | For example: 8 | 9 | ```js 10 | 11 | var oauth = oauthserver({ 12 | model: require('./model'), 13 | grants: ['password'], 14 | debug: true 15 | }); 16 | 17 | ``` 18 | 19 | ## Note 20 | 21 | In this example, the postgres connection info is read from the `DATABASE_URL` environment variable which you can set when you run, for example: 22 | 23 | ``` 24 | $ DATABASE_URL=postgres://postgres:1234@localhost/postgres node index.js 25 | ``` -------------------------------------------------------------------------------- /examples/mongodb/Readme.md: -------------------------------------------------------------------------------- 1 | # MongoDB Example 2 | 3 | You will need to initialize a Mongoose connection to a mongo db beforehand. 4 | 5 | For example : 6 | 7 | ```js 8 | 9 | var mongoose = require('mongoose'); 10 | 11 | var uristring = 'mongodb://localhost/test'; 12 | 13 | // Makes connection asynchronously. Mongoose will queue up database 14 | // operations and release them when the connection is complete. 15 | mongoose.connect(uristring, function (err, res) { 16 | if (err) { 17 | console.log ('ERROR connecting to: ' + uristring + '. ' + err); 18 | } else { 19 | console.log ('Succeeded connected to: ' + uristring); 20 | } 21 | }); 22 | 23 | ``` 24 | 25 | The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. 26 | 27 | For example: 28 | 29 | ```js 30 | 31 | app.oauth = oauthserver({ 32 | model: require('./model'), 33 | grants: ['password'], 34 | debug: true 35 | }); 36 | 37 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2-server-restify", 3 | "description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with restify in node.js", 4 | "version": "2.3.2", 5 | "keywords": [ 6 | "oauth", 7 | "oauth2" 8 | ], 9 | "author": { 10 | "name": "Thom Seddon", 11 | "email": "thom@seddonmedia.co.uk" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Marcos Sanz Latorre", 16 | "email": "marcos.sanz@13genius.com" 17 | } 18 | ], 19 | "main": "lib/oauth2server.js", 20 | "dependencies": { 21 | "node-oauth2-server-restify": "git+https://github.com/marsanla/node-oauth2-server-restify.git", 22 | "basic-auth": "~0.0.1", 23 | "node-restify-errors": "~0.1.0" 24 | }, 25 | "devDependencies": { 26 | "body-parser": "~1.3.1", 27 | "express": "~4.4.3", 28 | "mocha": "~1.20.1", 29 | "should": "~4.0.4", 30 | "supertest": "~0.13.0" 31 | }, 32 | "licenses": [ 33 | { 34 | "type": "Apache 2.0", 35 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 36 | } 37 | ], 38 | "engines": { 39 | "node": ">=0.8" 40 | }, 41 | "scripts": { 42 | "test": "mocha" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/marsanla/node-oauth2-server-restify.git" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var crypto = require('crypto'), 18 | error = require('node-restify-errors'); 19 | 20 | module.exports = Token; 21 | 22 | /** 23 | * Token generator that will delegate to model or 24 | * the internal random generator 25 | * 26 | * @param {String} type 'accessToken' or 'refreshToken' 27 | * @param {Function} callback 28 | */ 29 | function Token (config, type, callback) { 30 | if (config.model.generateToken) { 31 | config.model.generateToken(type, config.req, function (err, token) { 32 | if (err) return callback(new error.InternalError(err)); 33 | if (!token) return generateRandomToken(callback); 34 | callback(false, token); 35 | }); 36 | } else { 37 | generateRandomToken(callback); 38 | } 39 | } 40 | 41 | /** 42 | * Internal random token generator 43 | * 44 | * @param {Function} callback 45 | */ 46 | var generateRandomToken = function (callback) { 47 | crypto.randomBytes(256, function (ex, buffer) { 48 | if (ex) return callback(new error.InternalError()); 49 | 50 | var token = crypto 51 | .createHash('sha1') 52 | .update(buffer) 53 | .digest('hex'); 54 | 55 | callback(false, token); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | 3 | var OAuth2Error = require('../lib/error'); 4 | 5 | describe('OAuth2Error', function() { 6 | 7 | it('should be an instance of `Error`', function () { 8 | var error = new OAuth2Error('invalid_request', 'The access token was not found'); 9 | 10 | error.should.be.instanceOf(Error); 11 | }); 12 | 13 | it('should expose the `message` as the description', function () { 14 | var error = new OAuth2Error('invalid_request', 'The access token was not found'); 15 | 16 | error.message.should.equal('The access token was not found'); 17 | }); 18 | 19 | it('should expose the `stack`', function () { 20 | var error = new OAuth2Error('invalid_request', 'The access token was not found'); 21 | 22 | error.stack.should.not.equal(undefined); 23 | }); 24 | 25 | it('should expose a custom `name`', function () { 26 | var error = new OAuth2Error(); 27 | 28 | error.name.should.equal('OAuth2Error'); 29 | }); 30 | 31 | it('should expose `headers` if error is `invalid_client`', function () { 32 | var error = new OAuth2Error('invalid_client'); 33 | 34 | error.headers.should.eql({ 'WWW-Authenticate': 'Basic realm="Service"' }); 35 | }); 36 | 37 | it('should expose a status `code`', function () { 38 | var error = new OAuth2Error('invalid_client'); 39 | 40 | error.code.should.be.instanceOf(Number); 41 | }); 42 | 43 | it('should expose the `error`', function () { 44 | var error = new OAuth2Error('invalid_client'); 45 | 46 | error.error.should.equal('invalid_client'); 47 | }); 48 | 49 | it('should expose the `error_description`', function () { 50 | var error = new OAuth2Error('invalid_client', 'The access token was not found'); 51 | 52 | error.error_description.should.equal('The access token was not found'); 53 | }); 54 | 55 | it('should expose the `stack` of a previous error', function () { 56 | var error = new OAuth2Error('invalid_request', 'The access token was not found', new Error()); 57 | 58 | error.stack.should.not.match(/^OAuth2Error/); 59 | error.stack.should.match(/^Error/); 60 | }); 61 | 62 | it('should expose the `message` of a previous error', function () { 63 | var error = new OAuth2Error('invalid_request', 'The access token was not found', new Error('foo')); 64 | 65 | error.message.should.equal('foo'); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /test/grant.client_credentials.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { 27 | model: {}, 28 | grants: ['client_credentials'] 29 | }); 30 | 31 | app.set('json spaces', 0); 32 | app.use(bodyParser()); 33 | 34 | app.all('/oauth/token', oauth.grant()); 35 | 36 | app.use(oauth.errorHandler()); 37 | 38 | return app; 39 | }; 40 | 41 | describe('Granting with client_credentials grant type', function () { 42 | 43 | // N.B. Client is authenticated earlier in request 44 | 45 | it('should detect invalid user', function (done) { 46 | var app = bootstrap({ 47 | model: { 48 | getClient: function (id, secret, callback) { 49 | callback(false, true); 50 | }, 51 | grantTypeAllowed: function (clientId, grantType, callback) { 52 | callback(false, true); 53 | }, 54 | getUserFromClient: function (clientId, clientSecret, callback) { 55 | clientId.should.equal('thom'); 56 | clientSecret.should.equal('nightworld'); 57 | callback(false, false); // Fake invalid user 58 | } 59 | }, 60 | grants: ['client_credentials'] 61 | }); 62 | 63 | request(app) 64 | .post('/oauth/token') 65 | .set('Content-Type', 'application/x-www-form-urlencoded') 66 | .send({ 67 | grant_type: 'client_credentials' 68 | }) 69 | .set('Authorization', 'Basic dGhvbTpuaWdodHdvcmxk') 70 | .expect(400, /client credentials are invalid/i, done); 71 | 72 | }); 73 | }); -------------------------------------------------------------------------------- /test/lockdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(); 26 | app.oauth = oauth2server(oauthConfig || { 27 | model: {} 28 | }); 29 | 30 | app.use(bodyParser()); 31 | 32 | app.all('/oauth/token', app.oauth.grant); 33 | 34 | app.all('/private', function (req, res, next) { 35 | res.send('Hello'); 36 | }); 37 | 38 | app.all('/public', app.oauth.bypass, function (req, res, next) { 39 | res.send('Hello'); 40 | }); 41 | 42 | 43 | app.oauth.lockdown(app); 44 | 45 | app.use(app.oauth.errorHandler()); 46 | 47 | return app; 48 | }; 49 | 50 | describe('Lockdown pattern', function() { 51 | 52 | it('should substitute grant', function (done) { 53 | var app = bootstrap(); 54 | 55 | request(app) 56 | .get('/oauth/token') 57 | .expect(400, /method must be POST/i, done); 58 | }); 59 | 60 | it('should insert authorise by default', function (done) { 61 | var app = bootstrap(); 62 | 63 | request(app) 64 | .get('/private') 65 | .expect(400, /access token was not found/i, done); 66 | }); 67 | 68 | it('should pass valid request through authorise', function (done) { 69 | var app = bootstrap({ 70 | model: { 71 | getAccessToken: function (token, callback) { 72 | callback(token !== 'thom', { access_token: token, expires: null }); 73 | } 74 | } 75 | }); 76 | 77 | request(app) 78 | .get('/private?access_token=thom') 79 | .expect(200, /hello/i, done); 80 | }); 81 | 82 | it('should correctly bypass', function (done) { 83 | var app = bootstrap(); 84 | 85 | request(app) 86 | .get('/public') 87 | .expect(200, /hello/i, done); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/errorHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { model: {} }); 27 | 28 | app.use(bodyParser()); 29 | 30 | app.all('/oauth/token', oauth.grant()); 31 | app.all('/', oauth.authorise(), function (req, res) { 32 | res.send('Hello World'); 33 | }); 34 | 35 | app.use(oauth.errorHandler()); 36 | if (oauthConfig && oauthConfig.passthroughErrors) { 37 | app.use(function (err, req, res, next) { 38 | res.send('passthrough'); 39 | }); 40 | } 41 | 42 | return app; 43 | }; 44 | 45 | describe('Error Handler', function() { 46 | it('should return an oauth conformat response', function (done) { 47 | var app = bootstrap(); 48 | 49 | request(app) 50 | .get('/') 51 | .expect(400) 52 | .end(function (err, res) { 53 | if (err) return done(err); 54 | 55 | res.body.should.have.keys('code', 'error', 'error_description'); 56 | 57 | res.body.code.should.be.instanceOf(Number); 58 | res.body.code.should.equal(res.statusCode); 59 | 60 | res.body.error.should.be.instanceOf(String); 61 | 62 | res.body.error_description.should.be.instanceOf(String); 63 | 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should passthrough authorise errors', function (done) { 69 | var app = bootstrap({ 70 | passthroughErrors: true, 71 | model: {} 72 | }); 73 | 74 | request(app) 75 | .get('/') 76 | .expect(200, /^passthrough$/, done); 77 | }); 78 | 79 | it('should passthrough grant errors', function (done) { 80 | var app = bootstrap({ 81 | passthroughErrors: true, 82 | model: {} 83 | }); 84 | 85 | request(app) 86 | .post('/oauth/token') 87 | .expect(200, /^passthrough$/, done); 88 | }); 89 | }); -------------------------------------------------------------------------------- /examples/dynamodb/dal.js: -------------------------------------------------------------------------------- 1 | var Dal = function () { 2 | this.AWS = require('aws-sdk'); 3 | this.AWS.config.loadFromPath(__dirname + '/aws.json'); 4 | //change the endpoint to match your dynamodb endpoint 5 | this.db = new this.AWS.DynamoDB({ 6 | endpoint: "https://dynamodb.us-east-1.amazonaws.com/" 7 | }); 8 | }; 9 | 10 | Dal.prototype = { 11 | decodeValue: function (map) { 12 | if (typeof map.N !== 'undefined') 13 | return parseFloat(map.N); 14 | if (typeof map.S !== 'undefined') 15 | return map.S; 16 | return map.Bl; 17 | }, 18 | decodeValues: function (obj, vals) { 19 | var self = this; 20 | for (var key in vals) { 21 | if (vals.hasOwnProperty(key)) { 22 | obj[key] = self.decodeValue(vals[key]); 23 | } 24 | } 25 | }, 26 | formatAttributes: function (obj) { 27 | var item = {}; 28 | for (var p in obj) { 29 | if (obj.hasOwnProperty(p)) { 30 | if (obj[p] === 0 || typeof obj[p] == "number") { 31 | item[p] = {"N": obj[p].toString()}; 32 | } 33 | else { 34 | item[p] = {"S": obj[p]}; 35 | } 36 | } 37 | } 38 | return item; 39 | }, 40 | deleteEmptyProperties: function (obj) { 41 | //DynamoDB does not allow you to store empty values 42 | for (var p in obj) { 43 | if (!obj.hasOwnProperty(p)) 44 | continue; 45 | if (obj[p] !== 0 && (obj[p] === null || obj[p] === "")) { 46 | delete(obj[p]); 47 | } 48 | } 49 | }, 50 | doGet: function (tableName, keyHash, consistent, callback) { 51 | consistent = typeof consistent !== 'undefined' ? consistent : false; 52 | var self = this; 53 | var result = this.db.getItem({ 54 | TableName: tableName, 55 | Key: keyHash, 56 | ConsistentRead: consistent 57 | }, function (err, data) { 58 | var obj = {}; 59 | if (err !== null) { 60 | if (callback) callback(err, null); 61 | return; 62 | } 63 | if (typeof data.Item != "undefined") { 64 | self.decodeValues(obj, data.Item); 65 | } 66 | if (callback) callback(err, obj); 67 | }); 68 | }, 69 | doSet: function (obj, tableName, keyHash, callback) { 70 | this.deleteEmptyProperties(obj); 71 | this.db.putItem({ 72 | TableName: tableName, 73 | Item: this.formatAttributes(obj) 74 | }, function (err, data) { 75 | 76 | if (err !== null) return callback(err, null); 77 | callback(err, obj); 78 | }); 79 | }, 80 | doDelete: function (tableName, keyHash, callback) { 81 | this.db.deleteItem({ 82 | TableName: tableName, 83 | Key: keyHash 84 | }, 85 | function (err, data) { 86 | if (err !== null) { 87 | console.log(err); 88 | } 89 | if (callback) callback(err, data); 90 | }); 91 | } 92 | }; 93 | 94 | module.exports = new Dal(); 95 | -------------------------------------------------------------------------------- /examples/dynamodb/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | bodyParser = require('body-parser'), 3 | oauthserver = require('../../'); // Would be: 'oauth2-server' 4 | 5 | var app = express(); 6 | 7 | app.use(bodyParser()); 8 | 9 | app.oauth = oauthserver({ 10 | model: require('./model'), 11 | grants: ['password', 'refresh_token'], 12 | debug: true 13 | }); 14 | 15 | // Handle token grant requests 16 | app.all('/oauth/token', app.oauth.grant()); 17 | 18 | // Show them the "do you authorise xyz app to access your content?" page 19 | app.get('/oauth/authorise', function (req, res, next) { 20 | if (!req.session.user) { 21 | // If they aren't logged in, send them to your own login implementation 22 | return res.redirect('/login?redirect=' + req.path + '&client_id=' + 23 | req.query.client_id + '&redirect_uri=' + req.query.redirect_uri); 24 | } 25 | 26 | res.render('authorise', { 27 | client_id: req.query.client_id, 28 | redirect_uri: req.query.redirect_uri 29 | }); 30 | }); 31 | 32 | // Handle authorise 33 | app.post('/oauth/authorise', function (req, res, next) { 34 | if (!req.session.user) { 35 | return res.redirect('/login?client_id=' + req.query.client_id + 36 | '&redirect_uri=' + req.query.redirect_uri); 37 | } 38 | 39 | next(); 40 | }, app.oauth.authCodeGrant(function (req, next) { 41 | // The first param should to indicate an error 42 | // The second param should a bool to indicate if the user did authorise the app 43 | // The third param should for the user/uid (only used for passing to saveAuthCode) 44 | next(null, req.body.allow === 'yes', req.session.user.id, req.session.user); 45 | })); 46 | 47 | // Show login 48 | app.get('/login', function (req, res, next) { 49 | res.render('login', { 50 | redirect: req.query.redirect, 51 | client_id: req.query.client_id, 52 | redirect_uri: req.query.redirect_uri 53 | }); 54 | }); 55 | 56 | // Handle login 57 | app.post('/login', function (req, res, next) { 58 | // Insert your own login mechanism 59 | if (req.body.email !== 'thom@nightworld.com') { 60 | res.render('login', { 61 | redirect: req.body.redirect, 62 | client_id: req.body.client_id, 63 | redirect_uri: req.body.redirect_uri 64 | }); 65 | } else { 66 | // Successful logins should send the user back to the /oauth/authorise 67 | // with the client_id and redirect_uri (you could store these in the session) 68 | return res.redirect((req.body.redirect || '/home') + '?client_id=' + 69 | req.body.client_id + '&redirect_uri=' + req.body.redirect_uri); 70 | } 71 | }); 72 | 73 | app.get('/secret', app.oauth.authorise(), function (req, res) { 74 | // Will require a valid access_token 75 | res.send('Secret area'); 76 | }); 77 | 78 | app.get('/public', function (req, res) { 79 | // Does not require an access_token 80 | res.send('Public area'); 81 | }); 82 | 83 | // Error handling 84 | app.use(app.oauth.errorHandler()); 85 | 86 | app.listen(3000); 87 | -------------------------------------------------------------------------------- /examples/postgresql/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | bodyParser = require('body-parser'), 3 | oauthserver = require('../../'); // Would be: 'oauth2-server' 4 | 5 | var app = express(); 6 | 7 | app.use(bodyParser()); 8 | 9 | app.oauth = oauthserver({ 10 | model: require('./model'), 11 | grants: ['auth_code', 'password'], 12 | debug: true 13 | }); 14 | 15 | // Handle token grant requests 16 | app.all('/oauth/token', app.oauth.grant()); 17 | 18 | // Show them the "do you authorise xyz app to access your content?" page 19 | app.get('/oauth/authorise', function (req, res, next) { 20 | if (!req.session.user) { 21 | // If they aren't logged in, send them to your own login implementation 22 | return res.redirect('/login?redirect=' + req.path + '&client_id=' + 23 | req.query.client_id + '&redirect_uri=' + req.query.redirect_uri); 24 | } 25 | 26 | res.render('authorise', { 27 | client_id: req.query.client_id, 28 | redirect_uri: req.query.redirect_uri 29 | }); 30 | }); 31 | 32 | // Handle authorise 33 | app.post('/oauth/authorise', function (req, res, next) { 34 | if (!req.session.user) { 35 | return res.redirect('/login?client_id=' + req.query.client_id + 36 | '&redirect_uri=' + req.query.redirect_uri); 37 | } 38 | 39 | next(); 40 | }, app.oauth.authCodeGrant(function (req, next) { 41 | // The first param should to indicate an error 42 | // The second param should a bool to indicate if the user did authorise the app 43 | // The third param should for the user/uid (only used for passing to saveAuthCode) 44 | next(null, req.body.allow === 'yes', req.session.user.id, req.session.user); 45 | })); 46 | 47 | // Show login 48 | app.get('/login', function (req, res, next) { 49 | res.render('login', { 50 | redirect: req.query.redirect, 51 | client_id: req.query.client_id, 52 | redirect_uri: req.query.redirect_uri 53 | }); 54 | }); 55 | 56 | // Handle login 57 | app.post('/login', function (req, res, next) { 58 | // Insert your own login mechanism 59 | if (req.body.email !== 'thom@nightworld.com') { 60 | res.render('login', { 61 | redirect: req.body.redirect, 62 | client_id: req.body.client_id, 63 | redirect_uri: req.body.redirect_uri 64 | }); 65 | } else { 66 | // Successful logins should send the user back to the /oauth/authorise 67 | // with the client_id and redirect_uri (you could store these in the session) 68 | return res.redirect((req.body.redirect || '/home') + '?client_id=' + 69 | req.body.client_id + '&redirect_uri=' + req.body.redirect_uri); 70 | } 71 | }); 72 | 73 | app.get('/secret', app.oauth.authorise(), function (req, res) { 74 | // Will require a valid access_token 75 | res.send('Secret area'); 76 | }); 77 | 78 | app.get('/public', function (req, res) { 79 | // Does not require an access_token 80 | res.send('Public area'); 81 | }); 82 | 83 | // Error handling 84 | app.use(app.oauth.errorHandler()); 85 | 86 | app.listen(3000); 87 | -------------------------------------------------------------------------------- /examples/dynamodb/Readme.md: -------------------------------------------------------------------------------- 1 | # DynamoDB Example 2 | 3 | requires [`aws-sdk`](http://aws.amazon.com/sdkfornodejs/) 4 | 5 | You will need to create the required tables (see below): 6 | 7 | The object exposed in model.js could be directly passed into the model parameter of the config object when initiating. 8 | 9 | For example: 10 | 11 | ```js 12 | ... 13 | 14 | app.oauth = oauthserver({ 15 | model: require('./model'), 16 | grants: ['password', 'refresh_token'], 17 | debug: true 18 | }); 19 | 20 | ... 21 | ``` 22 | 23 | 24 | #### Creating required tables in DynamoDB 25 | 26 | ```js 27 | // 28 | // Table definitions 29 | // 30 | var OAuth2AccessToken = { 31 | AttributeDefinitions: [ 32 | { 33 | AttributeName: "accessToken", 34 | AttributeType: "S" 35 | } 36 | ], 37 | TableName: "oauth2accesstoken", 38 | KeySchema: [ 39 | { 40 | AttributeName: "accessToken", 41 | KeyType: "HASH" 42 | } 43 | ], 44 | ProvisionedThroughput: { 45 | ReadCapacityUnits: 12, 46 | WriteCapacityUnits: 6 47 | } 48 | }; 49 | 50 | var OAuth2RefreshToken = { 51 | AttributeDefinitions: [ 52 | { 53 | AttributeName: "refreshToken", 54 | AttributeType: "S" 55 | } 56 | ], 57 | TableName: "oauth2refreshtoken", 58 | KeySchema: [ 59 | { 60 | AttributeName: "refreshToken", 61 | KeyType: "HASH" 62 | } 63 | ], 64 | ProvisionedThroughput: { 65 | ReadCapacityUnits: 6, 66 | WriteCapacityUnits: 6 67 | } 68 | }; 69 | 70 | var OAuth2AuthCode = { 71 | AttributeDefinitions: [ 72 | { 73 | AttributeName: "authCode", 74 | AttributeType: "S" 75 | } 76 | ], 77 | TableName: "oauth2authcode", 78 | KeySchema: [ 79 | { 80 | AttributeName: "authCode", 81 | KeyType: "HASH" 82 | } 83 | ], 84 | ProvisionedThroughput: { 85 | ReadCapacityUnits: 6, 86 | WriteCapacityUnits: 6 87 | } 88 | }; 89 | 90 | var OAuth2Client = { 91 | AttributeDefinitions: [ 92 | { 93 | AttributeName: "clientId", 94 | AttributeType: "S" 95 | } 96 | ], 97 | TableName: "oauth2client", 98 | KeySchema: [ 99 | { 100 | AttributeName: "clientId", 101 | KeyType: "HASH" 102 | } 103 | ], 104 | ProvisionedThroughput: { 105 | ReadCapacityUnits: 6, 106 | WriteCapacityUnits: 6 107 | } 108 | }; 109 | 110 | 111 | var OAuth2User = { 112 | AttributeDefinitions: [ 113 | { 114 | AttributeName: "username", 115 | AttributeType: "S" 116 | }, 117 | { 118 | AttributeName: "password", 119 | AttributeType: "S" 120 | } 121 | ], 122 | TableName: "oauth2user", 123 | KeySchema: [ 124 | { 125 | AttributeName: "username", 126 | KeyType: "HASH" 127 | }, 128 | { 129 | AttributeName: "password", 130 | KeyType: "RANGE" 131 | } 132 | ], 133 | ProvisionedThroughput: { 134 | ReadCapacityUnits: 6, 135 | WriteCapacityUnits: 6 136 | } 137 | }; 138 | ``` 139 | -------------------------------------------------------------------------------- /examples/postgresql/schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | SET statement_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET check_function_bodies = false; 9 | SET client_min_messages = warning; 10 | 11 | -- 12 | -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - 13 | -- 14 | 15 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 16 | 17 | 18 | -- 19 | -- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - 20 | -- 21 | 22 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 23 | 24 | 25 | SET search_path = public, pg_catalog; 26 | 27 | SET default_tablespace = ''; 28 | 29 | SET default_with_oids = false; 30 | 31 | -- 32 | -- Name: oauth_access_tokens; Type: TABLE; Schema: public; Owner: -; Tablespace: 33 | -- 34 | 35 | CREATE TABLE oauth_access_tokens ( 36 | access_token text NOT NULL, 37 | client_id text NOT NULL, 38 | user_id uuid NOT NULL, 39 | expires timestamp without time zone NOT NULL 40 | ); 41 | 42 | 43 | -- 44 | -- Name: oauth_clients; Type: TABLE; Schema: public; Owner: -; Tablespace: 45 | -- 46 | 47 | CREATE TABLE oauth_clients ( 48 | client_id text NOT NULL, 49 | client_secret text NOT NULL, 50 | redirect_uri text NOT NULL 51 | ); 52 | 53 | 54 | -- 55 | -- Name: oauth_refresh_tokens; Type: TABLE; Schema: public; Owner: -; Tablespace: 56 | -- 57 | 58 | CREATE TABLE oauth_refresh_tokens ( 59 | refresh_token text NOT NULL, 60 | client_id text NOT NULL, 61 | user_id uuid NOT NULL, 62 | expires timestamp without time zone NOT NULL 63 | ); 64 | 65 | 66 | -- 67 | -- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace: 68 | -- 69 | 70 | CREATE TABLE users ( 71 | id uuid NOT NULL, 72 | username text NOT NULL, 73 | password text NOT NULL 74 | ); 75 | 76 | 77 | -- 78 | -- Name: oauth_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: 79 | -- 80 | 81 | ALTER TABLE ONLY oauth_access_tokens 82 | ADD CONSTRAINT oauth_access_tokens_pkey PRIMARY KEY (access_token); 83 | 84 | 85 | -- 86 | -- Name: oauth_clients_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: 87 | -- 88 | 89 | ALTER TABLE ONLY oauth_clients 90 | ADD CONSTRAINT oauth_clients_pkey PRIMARY KEY (client_id, client_secret); 91 | 92 | 93 | -- 94 | -- Name: oauth_refresh_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: 95 | -- 96 | 97 | ALTER TABLE ONLY oauth_refresh_tokens 98 | ADD CONSTRAINT oauth_refresh_tokens_pkey PRIMARY KEY (refresh_token); 99 | 100 | 101 | -- 102 | -- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: 103 | -- 104 | 105 | ALTER TABLE ONLY users 106 | ADD CONSTRAINT users_pkey PRIMARY KEY (id); 107 | 108 | 109 | -- 110 | -- Name: users_username_password; Type: INDEX; Schema: public; Owner: -; Tablespace: 111 | -- 112 | 113 | CREATE INDEX users_username_password ON users USING btree (username, password); 114 | 115 | 116 | -- 117 | -- PostgreSQL database dump complete 118 | -- 119 | 120 | -------------------------------------------------------------------------------- /test/grant.password.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { 27 | model: {}, 28 | grants: ['password', 'refresh_token'] 29 | }); 30 | 31 | app.set('json spaces', 0); 32 | app.use(bodyParser()); 33 | 34 | app.all('/oauth/token', oauth.grant()); 35 | 36 | app.use(oauth.errorHandler()); 37 | 38 | return app; 39 | }; 40 | 41 | describe('Granting with password grant type', function () { 42 | it('should detect missing parameters', function (done) { 43 | var app = bootstrap({ 44 | model: { 45 | getClient: function (id, secret, callback) { 46 | callback(false, true); 47 | }, 48 | grantTypeAllowed: function (clientId, grantType, callback) { 49 | callback(false, true); 50 | } 51 | }, 52 | grants: ['password'] 53 | }); 54 | 55 | request(app) 56 | .post('/oauth/token') 57 | .set('Content-Type', 'application/x-www-form-urlencoded') 58 | .send({ 59 | grant_type: 'password', 60 | client_id: 'thom', 61 | client_secret: 'nightworld' 62 | }) 63 | .expect(400, /missing parameters. \\"username\\" and \\"password\\"/i, done); 64 | 65 | }); 66 | 67 | it('should detect invalid user', function (done) { 68 | var app = bootstrap({ 69 | model: { 70 | getClient: function (id, secret, callback) { 71 | callback(false, true); 72 | }, 73 | grantTypeAllowed: function (clientId, grantType, callback) { 74 | callback(false, true); 75 | }, 76 | getUser: function (uname, pword, callback) { 77 | uname.should.equal('thomseddon'); 78 | pword.should.equal('nightworld'); 79 | callback(false, false); // Fake invalid user 80 | } 81 | }, 82 | grants: ['password'] 83 | }); 84 | 85 | request(app) 86 | .post('/oauth/token') 87 | .set('Content-Type', 'application/x-www-form-urlencoded') 88 | .send({ 89 | grant_type: 'password', 90 | client_id: 'thom', 91 | client_secret: 'nightworld', 92 | username: 'thomseddon', 93 | password: 'nightworld' 94 | }) 95 | .expect(400, /user credentials are invalid/i, done); 96 | 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /examples/memory/model.js: -------------------------------------------------------------------------------- 1 | var model = module.exports; 2 | 3 | // In-memory datastores: 4 | var oauthAccessTokens = [], 5 | oauthRefreshTokens = [], 6 | oauthClients = [ 7 | { 8 | clientId : 'thom', 9 | clientSecret : 'nightworld', 10 | redirectUri : '' 11 | } 12 | ], 13 | authorizedClientIds = { 14 | password: [ 15 | 'thom' 16 | ], 17 | refresh_token: [ 18 | 'thom' 19 | ] 20 | }, 21 | users = [ 22 | { 23 | id : '123', 24 | username: 'thomseddon', 25 | password: 'nightworld' 26 | } 27 | ]; 28 | 29 | // Debug function to dump the state of the data stores 30 | model.dump = function() { 31 | console.log('oauthAccessTokens', oauthAccessTokens); 32 | console.log('oauthClients', oauthClients); 33 | console.log('authorizedClientIds', authorizedClientIds); 34 | console.log('oauthRefreshTokens', oauthRefreshTokens); 35 | console.log('users', users); 36 | }; 37 | 38 | /* 39 | * Required 40 | */ 41 | 42 | model.getAccessToken = function (bearerToken, callback) { 43 | for(var i = 0, len = oauthAccessTokens.length; i < len; i++) { 44 | var elem = oauthAccessTokens[i]; 45 | if(elem.accessToken === bearerToken) { 46 | return callback(false, elem); 47 | } 48 | } 49 | callback(false, false); 50 | }; 51 | 52 | model.getRefreshToken = function (bearerToken, callback) { 53 | for(var i = 0, len = oauthRefreshTokens.length; i < len; i++) { 54 | var elem = oauthRefreshTokens[i]; 55 | if(elem.refreshToken === bearerToken) { 56 | return callback(false, elem); 57 | } 58 | } 59 | callback(false, false); 60 | }; 61 | 62 | model.getClient = function (clientId, clientSecret, callback) { 63 | for(var i = 0, len = oauthClients.length; i < len; i++) { 64 | var elem = oauthClients[i]; 65 | if(elem.clientId === clientId && 66 | (clientSecret === null || elem.clientSecret === clientSecret)) { 67 | return callback(false, elem); 68 | } 69 | } 70 | callback(false, false); 71 | }; 72 | 73 | model.grantTypeAllowed = function (clientId, grantType, callback) { 74 | callback(false, authorizedClientIds[grantType] && 75 | authorizedClientIds[grantType].indexOf(clientId.toLowerCase()) >= 0); 76 | }; 77 | 78 | model.saveAccessToken = function (accessToken, clientId, expires, userId, callback) { 79 | oauthAccessTokens.unshift({ 80 | accessToken: accessToken, 81 | clientId: clientId, 82 | userId: userId, 83 | expires: expires 84 | }); 85 | 86 | callback(false); 87 | }; 88 | 89 | model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { 90 | oauthRefreshTokens.unshift({ 91 | refreshToken: refreshToken, 92 | clientId: clientId, 93 | userId: userId, 94 | expires: expires 95 | }); 96 | 97 | callback(false); 98 | }; 99 | 100 | /* 101 | * Required to support password grant type 102 | */ 103 | model.getUser = function (username, password, callback) { 104 | for(var i = 0, len = users.length; i < len; i++) { 105 | var elem = users[i]; 106 | if(elem.username === username && elem.password === password) { 107 | return callback(false, elem); 108 | } 109 | } 110 | callback(false, false); 111 | }; 112 | -------------------------------------------------------------------------------- /npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ 'node', '/usr/local/bin/npm', 'publish' ] 3 | 2 info using npm@1.4.28 4 | 3 info using node@v0.10.32 5 | 4 verbose publish [ '.' ] 6 | 5 verbose cache add [ '.', null ] 7 | 6 verbose cache add name=undefined spec="." args=[".",null] 8 | 7 verbose parsed url { protocol: null, 9 | 7 verbose parsed url slashes: null, 10 | 7 verbose parsed url auth: null, 11 | 7 verbose parsed url host: null, 12 | 7 verbose parsed url port: null, 13 | 7 verbose parsed url hostname: null, 14 | 7 verbose parsed url hash: null, 15 | 7 verbose parsed url search: null, 16 | 7 verbose parsed url query: null, 17 | 7 verbose parsed url pathname: '.', 18 | 7 verbose parsed url path: '.', 19 | 7 verbose parsed url href: '.' } 20 | 8 silly lockFile 3a52ce78- . 21 | 9 verbose lock . /Users/marsanla/.npm/3a52ce78-.lock 22 | 10 verbose tar pack [ '/Users/marsanla/.npm/oauth2-server-restify/2.3.0/package.tgz', 23 | 10 verbose tar pack '.' ] 24 | 11 verbose tarball /Users/marsanla/.npm/oauth2-server-restify/2.3.0/package.tgz 25 | 12 verbose folder . 26 | 13 info prepublish oauth2-server-restify@2.3.0 27 | 14 silly lockFile 1f1177db-tar tar://. 28 | 15 verbose lock tar://. /Users/marsanla/.npm/1f1177db-tar.lock 29 | 16 silly lockFile d23e35c7-server-restify-2-3-0-package-tgz tar:///Users/marsanla/.npm/oauth2-server-restify/2.3.0/package.tgz 30 | 17 verbose lock tar:///Users/marsanla/.npm/oauth2-server-restify/2.3.0/package.tgz /Users/marsanla/.npm/d23e35c7-server-restify-2-3-0-package-tgz.lock 31 | 18 silly lockFile 1f1177db-tar tar://. 32 | 19 silly lockFile 1f1177db-tar tar://. 33 | 20 silly lockFile d23e35c7-server-restify-2-3-0-package-tgz tar:///Users/marsanla/.npm/oauth2-server-restify/2.3.0/package.tgz 34 | 21 silly lockFile d23e35c7-server-restify-2-3-0-package-tgz tar:///Users/marsanla/.npm/oauth2-server-restify/2.3.0/package.tgz 35 | 22 silly lockFile e9adc364-th2-server-restify-2-3-0-package /Users/marsanla/.npm/oauth2-server-restify/2.3.0/package 36 | 23 verbose lock /Users/marsanla/.npm/oauth2-server-restify/2.3.0/package /Users/marsanla/.npm/e9adc364-th2-server-restify-2-3-0-package.lock 37 | 24 silly lockFile e9adc364-th2-server-restify-2-3-0-package /Users/marsanla/.npm/oauth2-server-restify/2.3.0/package 38 | 25 silly lockFile e9adc364-th2-server-restify-2-3-0-package /Users/marsanla/.npm/oauth2-server-restify/2.3.0/package 39 | 26 silly lockFile 3a52ce78- . 40 | 27 silly lockFile 3a52ce78- . 41 | 28 error addLocal Could not install . 42 | 29 error Error: EACCES, open '/Users/marsanla/.npm/oauth2-server-restify/2.3.0/package/package.json' 43 | 29 error { [Error: EACCES, open '/Users/marsanla/.npm/oauth2-server-restify/2.3.0/package/package.json'] 44 | 29 error errno: 3, 45 | 29 error code: 'EACCES', 46 | 29 error path: '/Users/marsanla/.npm/oauth2-server-restify/2.3.0/package/package.json' } 47 | 30 error Please try running this command again as root/Administrator. 48 | 31 error System Darwin 14.0.0 49 | 32 error command "node" "/usr/local/bin/npm" "publish" 50 | 33 error cwd /Users/marsanla/Proyectos/elefrant/components/elefrant-oauth2/node_modules/oauth2-server-restify 51 | 34 error node -v v0.10.32 52 | 35 error npm -v 1.4.28 53 | 36 error path /Users/marsanla/.npm/oauth2-server-restify/2.3.0/package/package.json 54 | 37 error code EACCES 55 | 38 error errno 3 56 | 39 error stack Error: EACCES, open '/Users/marsanla/.npm/oauth2-server-restify/2.3.0/package/package.json' 57 | 40 verbose exit [ 3, true ] 58 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 2.3.0 4 | 5 | - Support "state" param for auth_code grant type 6 | - Docs for client_credentials grant type 7 | - Fix `getRefreshToken` in postgres model example 8 | 9 | ### 2.2.2 10 | 11 | - Fix bug when client has multiple redirect_uri's (#84) 12 | 13 | ### 2.2.1 14 | 15 | - Fix node 0.8.x (well npm 1.2.x) support 16 | 17 | ### 2.2.0 18 | 19 | - Support custom loggers via `debug` param 20 | - Make OAuth2Error inherit from Error for fun and profit 21 | - Don't go crazy when body is `null` 22 | - Update tests and examples to express 4 23 | - Fix lockdown pattern for express 4 24 | - Update dev dependencies (mocha, should and supertest) 25 | 26 | ### 2.1.1 27 | 28 | - Allow client to return an array of multiple valid redirect URI's 29 | - Fix continueAfterResponse when granting 30 | 31 | ### 2.1.0 32 | - Add support for client_credentials grant type (@lucknerjb) 33 | - Support Authorization grant via GET request (@mjsalinger) 34 | 35 | ### 2.0.2 36 | - Fix continueAfterResponse option 37 | 38 | ### 2.0.1 39 | - Add "WWW-Authenticate" header for invalid_client 40 | 41 | ### 2.0 42 | - Huge intrenal refactor 43 | - Switch from internal router ("allow" property) to exposing explit authorisation middleware to be added to individual routes 44 | - Expose grant middleware to be attached to a route of your choosing 45 | - Switch all model variables to camelCasing 46 | - Add support for `authorization_code` grant type (i.e. traditional "allow", "deny" with redirects etc.) 47 | - Some, previously wrong, error codes fixed 48 | 49 | ### 1.5.3 50 | - Fix tests for daylight saving 51 | 52 | ### 1.5.2 53 | - Fix expiration token checking (previously expires was wrongly checked against boot time) 54 | 55 | ### 1.5.1 56 | - Add repository field to package 57 | 58 | ### 1.5.0 59 | - Add support for non-expiring tokens (set accessTokenLifetime/refreshTokenLifetime = null) 60 | - Passthrough debug errors from custom generateToken 61 | 62 | ### 1.4.1 63 | - Allow access token in body when not POST (only deny GET) 64 | 65 | ### 1.4.0 66 | - Add support for refresh_token grant type 67 | 68 | ### 1.3.2 69 | - Require application/x-www-form-urlencoded when access token in body 70 | - Require authentication on both client id and secret 71 | 72 | ### 1.3.1 73 | - Fix client credentials extraction from Authorization header 74 | 75 | ### 1.3.0 76 | - Add passthroughErrors option 77 | - Optimise oauth.handler() with regex caching 78 | - Add PostgreSQL example 79 | - Allow req.user to be set by setting token.user in getAccessToken 80 | 81 | ### 1.2.5 82 | - Expose the token passed back from getAccessToken in req.token 83 | 84 | ### 1.2.4 85 | - Pass through Bad Request errors from connect 86 | 87 | ### 1.2.3 88 | - Fix generateToken override 89 | - Allow extended grant to pass back custom error 90 | 91 | ### 1.2.2 92 | - Fix reissuing 93 | 94 | ### 1.2.1 95 | - Allow token reissuing (Model can return an object to indicate a reissue, plain string (as in previous implementation) or null to revert to the default token generator) 96 | 97 | ### 1.2.0 98 | - Add optional generateToken method to model to allow custom token generation 99 | 100 | ### 1.1.1 101 | - Fix expired token checking 102 | 103 | ### 1.1.0 104 | - Add support for extension grants 105 | - Use async crypto.randomBytes in token generation 106 | - Refactor structure, break into more files 107 | -------------------------------------------------------------------------------- /lib/authorise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var error = require('node-restify-errors'), 18 | runner = require('./runner'); 19 | 20 | module.exports = Authorise; 21 | 22 | /** 23 | * This is the function order used by the runner 24 | * 25 | * @type {Array} 26 | */ 27 | var fns = [ 28 | getBearerToken, 29 | checkToken 30 | ]; 31 | 32 | /** 33 | * Authorise 34 | * 35 | * @param {Object} config Instance of OAuth object 36 | * @param {Object} req 37 | * @param {Object} res 38 | * @param {Function} next 39 | */ 40 | function Authorise (config, req, next) { 41 | this.config = config; 42 | this.model = config.model; 43 | this.req = req; 44 | 45 | runner(fns, this, next); 46 | } 47 | 48 | /** 49 | * Get bearer token 50 | * 51 | * Extract token from request according to RFC6750 52 | * 53 | * @param {Function} next 54 | * @this OAuth 55 | */ 56 | function getBearerToken (next) { 57 | var headerToken = this.req.authorization, 58 | getToken = this.req.query.access_token, 59 | postToken = this.req.body ? this.req.body.access_token : undefined; 60 | 61 | // Check exactly one method was used 62 | var methodsUsed = (headerToken !== undefined && Object.keys(headerToken).length > 0) + (getToken !== undefined) + 63 | (postToken !== undefined); 64 | 65 | if (methodsUsed > 1) { 66 | return next(new error.BadMethodError('Only one method may be used to authenticate at a time (Auth header, GET or POST).')); 67 | } else if (methodsUsed === 0) { 68 | return next(new error.InvalidCredentialsError('The access token was not found')); 69 | } 70 | 71 | // Header: http://tools.ietf.org/html/rfc6750#section-2.1 72 | if (headerToken && headerToken.credentials) { 73 | var matches = (headerToken.scheme === 'Bearer') ? headerToken.credentials : null; 74 | 75 | if (!matches) { 76 | return next(new error.InvalidHeaderError('Malformed auth header')); 77 | } 78 | 79 | headerToken = matches; 80 | } else { 81 | headerToken = undefined; 82 | } 83 | 84 | // POST: http://tools.ietf.org/html/rfc6750#section-2.2 85 | if (postToken) { 86 | if (this.req.method === 'GET') { 87 | return next(new error.BadMethodError('Method cannot be GET When putting the token in the body.')); 88 | } 89 | 90 | if (!this.req.is('application/x-www-form-urlencoded')) { 91 | return next(new error.BadMethodError('When putting the token in the ' + 92 | 'body, content type must be application/x-www-form-urlencoded.')); 93 | } 94 | } 95 | 96 | this.bearerToken = headerToken || postToken || getToken; 97 | next(); 98 | } 99 | 100 | /** 101 | * Check token 102 | * 103 | * Check it against model, ensure it's not expired 104 | * @param {Function} next 105 | * @this OAuth 106 | */ 107 | function checkToken (next) { 108 | var self = this; 109 | 110 | if (this.model && Object.keys(this.model).length > 0) { 111 | this.model.getAccessToken(this.bearerToken, function (err, token) { 112 | if (err) return next(new error.InternalError(err)); 113 | 114 | if (!token) { 115 | return next(new error.InvalidCredentialsError('The access token provided is invalid.')); 116 | } 117 | 118 | if (token.expires !== null && 119 | (!token.expires || token.expires < new Date())) { 120 | return next(new error.InvalidCredentialsError('The access token provided has expired.')); 121 | } 122 | 123 | // Expose params 124 | self.req.oauth = {bearerToken: token}; 125 | self.req.user = token.user ? token.user : {id: token.userId}; 126 | 127 | next(); 128 | }); 129 | } else { 130 | next(new error.InvalidCredentialsError('The access token can not be find.')); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/grant.extended.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { 27 | model: {}, 28 | grants: ['password', 'refresh_token'] 29 | }); 30 | 31 | app.set('json spaces', 0); 32 | app.use(bodyParser()); 33 | 34 | app.all('/oauth/token', oauth.grant()); 35 | 36 | app.use(oauth.errorHandler()); 37 | 38 | return app; 39 | }; 40 | 41 | describe('Granting with extended grant type', function () { 42 | it('should ignore if no extendedGrant method', function (done) { 43 | var app = bootstrap({ 44 | model: { 45 | getClient: function (id, secret, callback) { 46 | callback(false, true); 47 | }, 48 | grantTypeAllowed: function (clientId, grantType, callback) { 49 | callback(false, true); 50 | } 51 | }, 52 | grants: ['http://custom.com'] 53 | }); 54 | 55 | request(app) 56 | .post('/oauth/token') 57 | .set('Content-Type', 'application/x-www-form-urlencoded') 58 | .send({ 59 | grant_type: 'http://custom.com', 60 | client_id: 'thom', 61 | client_secret: 'nightworld' 62 | }) 63 | .expect(400, /invalid grant_type/i, done); 64 | }); 65 | 66 | it('should still detect unsupported grant_type', function (done) { 67 | var app = bootstrap({ 68 | model: { 69 | getClient: function (id, secret, callback) { 70 | callback(false, true); 71 | }, 72 | grantTypeAllowed: function (clientId, grantType, callback) { 73 | callback(false, true); 74 | }, 75 | extendedGrant: function (grantType, req, callback) { 76 | callback(false, false); 77 | } 78 | }, 79 | grants: ['http://custom.com'] 80 | }); 81 | 82 | request(app) 83 | .post('/oauth/token') 84 | .set('Content-Type', 'application/x-www-form-urlencoded') 85 | .send({ 86 | grant_type: 'http://custom.com', 87 | client_id: 'thom', 88 | client_secret: 'nightworld' 89 | }) 90 | .expect(400, /invalid grant_type/i, done); 91 | }); 92 | 93 | it('should require a user.id', function (done) { 94 | var app = bootstrap({ 95 | model: { 96 | getClient: function (id, secret, callback) { 97 | callback(false, true); 98 | }, 99 | grantTypeAllowed: function (clientId, grantType, callback) { 100 | callback(false, true); 101 | }, 102 | extendedGrant: function (grantType, req, callback) { 103 | callback(false, true, {}); // Fake empty user 104 | } 105 | }, 106 | grants: ['http://custom.com'] 107 | }); 108 | 109 | request(app) 110 | .post('/oauth/token') 111 | .set('Content-Type', 'application/x-www-form-urlencoded') 112 | .send({ 113 | grant_type: 'http://custom.com', 114 | client_id: 'thom', 115 | client_secret: 'nightworld' 116 | }) 117 | .expect(400, /invalid request/i, done); 118 | }); 119 | 120 | it('should passthrough valid request', function (done) { 121 | var app = bootstrap({ 122 | model: { 123 | getClient: function (id, secret, callback) { 124 | callback(false, true); 125 | }, 126 | grantTypeAllowed: function (clientId, grantType, callback) { 127 | callback(false, true); 128 | }, 129 | extendedGrant: function (grantType, req, callback) { 130 | callback(false, true, { id: 3 }); 131 | }, 132 | saveAccessToken: function () { 133 | done(); // That's enough 134 | } 135 | }, 136 | grants: ['http://custom.com'] 137 | }); 138 | 139 | request(app) 140 | .post('/oauth/token') 141 | .set('Content-Type', 'application/x-www-form-urlencoded') 142 | .send({ 143 | grant_type: 'http://custom.com', 144 | client_id: 'thom', 145 | client_secret: 'nightworld' 146 | }) 147 | .expect(200, done); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /examples/mongodb/model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var mongoose = require('mongoose'), 18 | Schema = mongoose.Schema, 19 | model = module.exports; 20 | 21 | // 22 | // Schemas definitions 23 | // 24 | var OAuthAccessTokensSchema = new Schema({ 25 | accessToken: { type: String }, 26 | clientId: { type: String }, 27 | userId: { type: String }, 28 | expires: { type: Date } 29 | }); 30 | 31 | var OAuthRefreshTokensSchema = new Schema({ 32 | refreshToken: { type: String }, 33 | clientId: { type: String }, 34 | userId: { type: String }, 35 | expires: { type: Date } 36 | }); 37 | 38 | var OAuthClientsSchema = new Schema({ 39 | clientId: { type: String }, 40 | clientSecret: { type: String }, 41 | redirectUri: { type: String } 42 | }); 43 | 44 | var OAuthUsersSchema = new Schema({ 45 | username: { type: String }, 46 | password: { type: String }, 47 | firstname: { type: String }, 48 | lastname: { type: String }, 49 | email: { type: String, default: '' } 50 | }); 51 | 52 | mongoose.model('OAuthAccessTokens', OAuthAccessTokensSchema); 53 | mongoose.model('OAuthRefreshTokens', OAuthRefreshTokensSchema); 54 | mongoose.model('OAuthClients', OAuthClientsSchema); 55 | mongoose.model('OAuthUsers', OAuthUsersSchema); 56 | 57 | var OAuthAccessTokensModel = mongoose.model('OAuthAccessTokens'), 58 | OAuthRefreshTokensModel = mongoose.model('OAuthRefreshTokens'), 59 | OAuthClientsModel = mongoose.model('OAuthClients'), 60 | OAuthUsersModel = mongoose.model('OAuthUsers'); 61 | 62 | // 63 | // oauth2-server callbacks 64 | // 65 | model.getAccessToken = function (bearerToken, callback) { 66 | console.log('in getAccessToken (bearerToken: ' + bearerToken + ')'); 67 | 68 | OAuthAccessTokensModel.findOne({ accessToken: bearerToken }, callback); 69 | }; 70 | 71 | model.getClient = function (clientId, clientSecret, callback) { 72 | console.log('in getClient (clientId: ' + clientId + ', clientSecret: ' + clientSecret + ')'); 73 | if (clientSecret === null) { 74 | return OAuthClientsModel.findOne({ clientId: clientId }, callback); 75 | } 76 | OAuthClientsModel.findOne({ clientId: clientId, clientSecret: clientSecret }, callback); 77 | }; 78 | 79 | // This will very much depend on your setup, I wouldn't advise doing anything exactly like this but 80 | // it gives an example of how to use the method to resrict certain grant types 81 | var authorizedClientIds = ['s6BhdRkqt3', 'toto']; 82 | model.grantTypeAllowed = function (clientId, grantType, callback) { 83 | console.log('in grantTypeAllowed (clientId: ' + clientId + ', grantType: ' + grantType + ')'); 84 | 85 | if (grantType === 'password') { 86 | return callback(false, authorizedClientIds.indexOf(clientId) >= 0); 87 | } 88 | 89 | callback(false, true); 90 | }; 91 | 92 | model.saveAccessToken = function (token, clientId, expires, userId, callback) { 93 | console.log('in saveAccessToken (token: ' + token + ', clientId: ' + clientId + ', userId: ' + userId + ', expires: ' + expires + ')'); 94 | 95 | var accessToken = new OAuthAccessTokensModel({ 96 | accessToken: token, 97 | clientId: clientId, 98 | userId: userId, 99 | expires: expires 100 | }); 101 | 102 | accessToken.save(callback); 103 | }; 104 | 105 | /* 106 | * Required to support password grant type 107 | */ 108 | model.getUser = function (username, password, callback) { 109 | console.log('in getUser (username: ' + username + ', password: ' + password + ')'); 110 | 111 | OAuthUsersModel.findOne({ username: username, password: password }, function(err, user) { 112 | if(err) return callback(err); 113 | callback(null, user._id); 114 | }); 115 | }; 116 | 117 | /* 118 | * Required to support refreshToken grant type 119 | */ 120 | model.saveRefreshToken = function (token, clientId, expires, userId, callback) { 121 | console.log('in saveRefreshToken (token: ' + token + ', clientId: ' + clientId +', userId: ' + userId + ', expires: ' + expires + ')'); 122 | 123 | var refreshToken = new OAuthRefreshTokensModel({ 124 | refreshToken: token, 125 | clientId: clientId, 126 | userId: userId, 127 | expires: expires 128 | }); 129 | 130 | refreshToken.save(callback); 131 | }; 132 | 133 | model.getRefreshToken = function (refreshToken, callback) { 134 | console.log('in getRefreshToken (refreshToken: ' + refreshToken + ')'); 135 | 136 | OAuthRefreshTokensModel.findOne({ refreshToken: refreshToken }, callback); 137 | }; 138 | -------------------------------------------------------------------------------- /examples/postgresql/model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var pg = require('pg'), 18 | model = module.exports, 19 | connString = process.env.DATABASE_URL; 20 | 21 | /* 22 | * Required 23 | */ 24 | 25 | model.getAccessToken = function (bearerToken, callback) { 26 | pg.connect(connString, function (err, client, done) { 27 | if (err) return callback(err); 28 | client.query('SELECT access_token, client_id, expires, user_id FROM oauth_access_tokens ' + 29 | 'WHERE access_token = $1', [bearerToken], function (err, result) { 30 | if (err || !result.rowCount) return callback(err); 31 | // This object will be exposed in req.oauth.token 32 | // The user_id field will be exposed in req.user (req.user = { id: "..." }) however if 33 | // an explicit user object is included (token.user, must include id) it will be exposed 34 | // in req.user instead 35 | var token = result.rows[0]; 36 | callback(null, { 37 | accessToken: token.access_token, 38 | clientId: token.client_id, 39 | expires: token.expires, 40 | userId: token.userId 41 | }); 42 | done(); 43 | }); 44 | }); 45 | }; 46 | 47 | model.getClient = function (clientId, clientSecret, callback) { 48 | pg.connect(connString, function (err, client, done) { 49 | if (err) return callback(err); 50 | 51 | client.query('SELECT client_id, client_secret, redirect_uri FROM oauth_clients WHERE ' + 52 | 'client_id = $1', [clientId], function (err, result) { 53 | if (err || !result.rowCount) return callback(err); 54 | 55 | var client = result.rows[0]; 56 | 57 | if (clientSecret !== null && client.client_secret !== clientSecret) return callback(); 58 | 59 | // This object will be exposed in req.oauth.client 60 | callback(null, { 61 | clientId: client.client_id, 62 | clientSecret: client.client_secret 63 | }); 64 | done(); 65 | }); 66 | }); 67 | }; 68 | 69 | model.getRefreshToken = function (bearerToken, callback) { 70 | pg.connect(connString, function (err, client, done) { 71 | if (err) return callback(err); 72 | client.query('SELECT refresh_token, client_id, expires, user_id FROM oauth_refresh_tokens ' + 73 | 'WHERE refresh_token = $1', [bearerToken], function (err, result) { 74 | // The returned user_id will be exposed in req.user.id 75 | callback(err, result.rowCount ? result.rows[0] : false); 76 | done(); 77 | }); 78 | }); 79 | }; 80 | 81 | // This will very much depend on your setup, I wouldn't advise doing anything exactly like this but 82 | // it gives an example of how to use the method to resrict certain grant types 83 | var authorizedClientIds = ['abc1', 'def2']; 84 | model.grantTypeAllowed = function (clientId, grantType, callback) { 85 | if (grantType === 'password') { 86 | return callback(false, authorizedClientIds.indexOf(clientId.toLowerCase()) >= 0); 87 | } 88 | 89 | callback(false, true); 90 | }; 91 | 92 | model.saveAccessToken = function (accessToken, clientId, expires, userId, callback) { 93 | pg.connect(connString, function (err, client, done) { 94 | if (err) return callback(err); 95 | client.query('INSERT INTO oauth_access_tokens(access_token, client_id, user_id, expires) ' + 96 | 'VALUES ($1, $2, $3, $4)', [accessToken, clientId, userId, expires], 97 | function (err, result) { 98 | callback(err); 99 | done(); 100 | }); 101 | }); 102 | }; 103 | 104 | model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { 105 | pg.connect(connString, function (err, client, done) { 106 | if (err) return callback(err); 107 | client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, user_id, ' + 108 | 'expires) VALUES ($1, $2, $3, $4)', [refreshToken, clientId, userId, expires], 109 | function (err, result) { 110 | callback(err); 111 | done(); 112 | }); 113 | }); 114 | }; 115 | 116 | /* 117 | * Required to support password grant type 118 | */ 119 | model.getUser = function (username, password, callback) { 120 | pg.connect(connString, function (err, client, done) { 121 | if (err) return callback(err); 122 | client.query('SELECT id FROM users WHERE username = $1 AND password = $2', [username, 123 | password], function (err, result) { 124 | callback(err, result.rowCount ? result.rows[0] : false); 125 | done(); 126 | }); 127 | }); 128 | }; 129 | -------------------------------------------------------------------------------- /lib/authCodeGrant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var error = require('node-restify-errors'), 18 | runner = require('./runner'), 19 | token = require('./token'); 20 | 21 | module.exports = AuthCodeGrant; 22 | 23 | /** 24 | * This is the function order used by the runner 25 | * 26 | * @type {Array} 27 | */ 28 | var fns = [ 29 | checkParams, 30 | checkClient, 31 | checkUserApproved, 32 | generateCode, 33 | saveAuthCode, 34 | redirect 35 | ]; 36 | 37 | /** 38 | * AuthCodeGrant 39 | * 40 | * @param {Object} config Instance of OAuth object 41 | * @param {Object} req 42 | * @param {Object} res 43 | * @param {Function} next 44 | */ 45 | function AuthCodeGrant(config, req, res, next, check) { 46 | this.config = config; 47 | this.model = config.model; 48 | this.req = req; 49 | this.res = res; 50 | this.check = check; 51 | 52 | var self = this; 53 | runner(fns, this, function (err) { 54 | if (err && res.oauthRedirect) { 55 | // Custom redirect error handler 56 | res.redirect(self.client.redirectUri + '?error=' + err.error + 57 | '&error_description=' + err.error_description + '&code=' + err.code); 58 | 59 | return self.config.continueAfterResponse ? next() : null; 60 | } 61 | 62 | next(err); 63 | }); 64 | } 65 | 66 | /** 67 | * Check Request Params 68 | * 69 | * @param {Function} next 70 | * @this OAuth 71 | */ 72 | function checkParams (next) { 73 | var body = this.req.body; 74 | var query = this.req.query; 75 | if (!body && !query) return next(new error.BadMethodError()); 76 | 77 | // Response type 78 | this.responseType = body.response_type || query.response_type; 79 | if (this.responseType !== 'code') { 80 | return next(new error.BadMethodError('Invalid response_type parameter (must be "code")')); 81 | } 82 | 83 | // Client 84 | this.clientId = body.client_id || query.client_id; 85 | if (!this.clientId) { 86 | return next(new error.BadMethodError('Invalid or missing client_id parameter')); 87 | } 88 | 89 | // Redirect URI 90 | this.redirectUri = body.redirect_uri || query.redirect_uri; 91 | if (!this.redirectUri) { 92 | return next(new error.BadMethodError('Invalid or missing redirect_uri parameter')); 93 | } 94 | 95 | next(); 96 | } 97 | 98 | /** 99 | * Check client against model 100 | * 101 | * @param {Function} next 102 | * @this OAuth 103 | */ 104 | function checkClient (next) { 105 | var self = this; 106 | this.model.getClient(this.clientId, null, function (err, client) { 107 | if (err) return next(new error.InternalError(err)); 108 | 109 | if (!client) { 110 | return next(new error.InvalidCredentialsError('Invalid client credentials')); 111 | } else if (Array.isArray(client.redirectUri)) { 112 | if (client.redirecturi.indexOf(self.redirectUri) === -1) { 113 | return next(new error.BadMethodError('redirect_uri does not match')); 114 | } 115 | client.redirecturi = self.redirectUri; 116 | } else if (client.redirecturi !== self.redirectUri) { 117 | return next(new error.BadMethodError('redirect_uri does not match')); 118 | } 119 | 120 | // The request contains valid params so any errors after this point 121 | // are redirected to the redirect_uri 122 | self.res.oauthRedirect = true; 123 | self.client = client; 124 | 125 | next(); 126 | }); 127 | } 128 | 129 | /** 130 | * Check client against model 131 | * 132 | * @param {Function} next 133 | * @this OAuth 134 | */ 135 | function checkUserApproved (next) { 136 | var self = this; 137 | this.check(this.req, function (err, allowed, user) { 138 | if (err) return new next(error.InternalError(err)); 139 | 140 | if (!allowed) { 141 | return next(new error.NotAuthorizedError('The user denied access to your application')); 142 | } 143 | 144 | self.user = user; 145 | next(); 146 | }); 147 | } 148 | 149 | /** 150 | * Check client against model 151 | * 152 | * @param {Function} next 153 | * @this OAuth 154 | */ 155 | function generateCode (next) { 156 | var self = this; 157 | token(this, 'authorization_code', function (err, code) { 158 | self.authCode = code; 159 | next(err); 160 | }); 161 | } 162 | 163 | /** 164 | * Check client against model 165 | * 166 | * @param {Function} next 167 | * @this OAuth 168 | */ 169 | function saveAuthCode (next) { 170 | var expires = new Date(); 171 | expires.setSeconds(expires.getSeconds() + this.config.authCodeLifetime); 172 | 173 | this.model.saveAuthCode(this.authCode, this.client.clientId || this.client.id, expires, 174 | this.user, function (err) { 175 | if (err) return new next(error.InternalError(err)); 176 | next(); 177 | }); 178 | } 179 | 180 | /** 181 | * Check client against model 182 | * 183 | * @param {Function} next 184 | * @this OAuth 185 | */ 186 | function redirect (next) { 187 | this.res.redirect(this.client.redirecturi + '?code=' + this.authCode + 188 | (this.req.query.state ? '&state=' + this.req.query.state : '')); 189 | 190 | if (this.config.continueAfterResponse) 191 | return next(); 192 | } 193 | -------------------------------------------------------------------------------- /examples/dynamodb/model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var dal = require('./dal.js'); 18 | model = module.exports; 19 | 20 | var OAuthAccessTokenTable = "oauth2accesstoken"; 21 | var OAuthAuthCodeTable = "oauth2authcode"; 22 | var OAuthRefreshTokenTable = "oauth2refreshtoken"; 23 | var OAuthClientTable = "oauth2client"; 24 | var OAuthUserTable = "userid_map"; 25 | 26 | // 27 | // oauth2-server callbacks 28 | // 29 | model.getAccessToken = function (bearerToken, callback) { 30 | console.log('in getAccessToken (bearerToken: ' + bearerToken + ')'); 31 | 32 | dal.doGet(OAuthAccessTokenTable, 33 | {"accessToken": {"S": bearerToken}}, true, function(err, data) { 34 | if (data && data.expires) { 35 | data.expires = new Date(data.expires * 1000); 36 | } 37 | callback(err, data); 38 | }); 39 | }; 40 | 41 | model.getClient = function (clientId, clientSecret, callback) { 42 | console.log('in getClient (clientId: ' + clientId + ', clientSecret: ' + clientSecret + ')'); 43 | dal.doGet(OAuthClientTable, { clientId: { S: clientId }}, true, 44 | function(err, data) { 45 | if (err || !data) return callback(err, data); 46 | 47 | if (clientSecret !== null && data.clientSecret !== clientSecret) { 48 | return callback(); 49 | } 50 | 51 | callback(null, data); 52 | }); 53 | }; 54 | 55 | // This will very much depend on your setup, I wouldn't advise doing anything exactly like this but 56 | // it gives an example of how to use the method to restrict certain grant types 57 | var authorizedClientIds = ['abc1', 'def2']; 58 | model.grantTypeAllowed = function (clientId, grantType, callback) { 59 | console.log('in grantTypeAllowed (clientId: ' + clientId + ', grantType: ' + grantType + ')'); 60 | 61 | if (grantType === 'password') { 62 | return callback(false, authorizedClientIds.indexOf(clientId) >= 0); 63 | } 64 | 65 | callback(false, true); 66 | }; 67 | 68 | model.saveAccessToken = function (accessToken, clientId, expires, user, callback) { 69 | console.log('in saveAccessToken (accessToken: ' + accessToken + ', clientId: ' + clientId + ', userId: ' + user.id + ', expires: ' + expires + ')'); 70 | 71 | var token = { 72 | accessToken: accessToken, 73 | clientId: clientId, 74 | userId: user.id 75 | }; 76 | 77 | if (expires) token.expires = parseInt(expires / 1000, 10); 78 | console.log('saving', token); 79 | 80 | dal.doSet(token, OAuthAccessTokenTable, { accessToken: { S: accessToken }}, callback); 81 | }; 82 | 83 | model.saveRefreshToken = function (refreshToken, clientId, expires, user, callback) { 84 | console.log('in saveRefreshToken (refreshToken: ' + refreshToken + ', clientId: ' + clientId + ', userId: ' + user.id + ', expires: ' + expires + ')'); 85 | 86 | var token = { 87 | refreshToken: refreshToken, 88 | clientId: clientId, 89 | userId: user.id 90 | }; 91 | 92 | if (expires) token.expires = parseInt(expires / 1000, 10); 93 | console.log('saving', token); 94 | 95 | dal.doSet(token, OAuthRefreshTokenTable, { refreshToken: { S: refreshToken }}, callback); 96 | }; 97 | 98 | model.getRefreshToken = function (bearerToken, callback) { 99 | console.log("in getRefreshToken (bearerToken: " + bearerToken + ")"); 100 | 101 | dal.doGet(OAuthRefreshTokenTable, { refreshToken: { S: bearerToken }}, true, function(err, data) { 102 | if (data && data.expires) { 103 | data.expires = new Date(data.expires * 1000); 104 | } 105 | callback(err, data); 106 | }); 107 | }; 108 | 109 | model.revokeRefreshToken = function(bearerToken, callback) { 110 | console.log("in revokeRefreshToken (bearerToken: " + bearerToken + ")"); 111 | 112 | dal.doDelete(OAuthRefreshTokenTable, { refreshToken: { S: bearerToken }}, callback); 113 | }; 114 | 115 | model.getAuthCode = function (bearerCode, callback) { 116 | console.log("in getAuthCode (bearerCode: " + bearerCode + ")"); 117 | 118 | dal.doGet(OAuthAuthCodeTable, { authCode: { S: bearerCode }}, true, function(err, data) { 119 | if (data && data.expires) { 120 | data.expires = new Date(data.expires * 1000); 121 | } 122 | callback(err, data); 123 | }); 124 | }; 125 | 126 | model.saveAuthCode = function (authCode, clientId, expires, user, callback) { 127 | console.log('in saveAuthCode (authCode: ' + authCode + ', clientId: ' + clientId + ', userId: ' + user.id + ', expires: ' + expires + ')'); 128 | 129 | var code = { 130 | authCode: authCode, 131 | clientId: clientId, 132 | userId: user.id 133 | }; 134 | 135 | if (expires) code.expires = parseInt(expires / 1000, 10); 136 | console.log("saving", code); 137 | 138 | dal.doSet(code, OAuthAuthCodeTable, { authCode: { S: authCode }}, callback); 139 | }; 140 | 141 | 142 | /* 143 | * Required to support password grant type 144 | */ 145 | model.getUser = function (username, password, callback) { 146 | console.log('in getUser (username: ' + username + ', password: ' + password + ')'); 147 | 148 | dal.doGet(OAuthUserTable, { id: { S: "email:" + username}}, true, function(err, data) { 149 | if (err) return callback(err); 150 | callback(null, { id: data.userId }); 151 | }); 152 | }; 153 | -------------------------------------------------------------------------------- /lib/oauth2server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var AuthCodeGrant = require('./authCodeGrant'), 18 | Authorise = require('./authorise'), 19 | Grant = require('./grant'); 20 | 21 | module.exports = OAuth2Server; 22 | 23 | /** 24 | * Constructor 25 | * 26 | * @param {Object} config Configuration object 27 | */ 28 | function OAuth2Server (config) { 29 | 30 | if (!(this instanceof OAuth2Server)) return new OAuth2Server(config); 31 | 32 | config = config || {}; 33 | 34 | if (!config.model) throw new Error('No model supplied to OAuth2Server'); 35 | this.model = config.model; 36 | 37 | this.grants = config.grants || []; 38 | this.debug = config.debug || function () {}; 39 | if (typeof this.debug !== 'function') { 40 | this.debug = console.log; 41 | } 42 | this.passthroughErrors = config.passthroughErrors; 43 | this.continueAfterResponse = config.continueAfterResponse; 44 | 45 | this.accessTokenLifetime = config.accessTokenLifetime !== undefined ? 46 | config.accessTokenLifetime : 3600; 47 | this.refreshTokenLifetime = config.refreshTokenLifetime !== undefined ? 48 | config.refreshTokenLifetime : 1209600; 49 | this.authCodeLifetime = config.authCodeLifetime || 30; 50 | 51 | this.regex = { 52 | clientId: config.clientIdRegex || /^[a-z0-9-_]{3,40}$/i, 53 | grantType: new RegExp('^(' + this.grants.join('|') + ')$', 'i') 54 | }; 55 | } 56 | 57 | /** 58 | * Authorisation Middleware 59 | * 60 | * Returns middleware that will authorise the request using oauth, 61 | * if successful it will allow the request to proceed to the next handler 62 | * 63 | * @return {Function} middleware 64 | */ 65 | OAuth2Server.prototype.authorise = function () { 66 | var self = this; 67 | 68 | return function (req, res, next) { 69 | new Authorise(self, req, next); 70 | }; 71 | }; 72 | 73 | /** 74 | * Check authorisation Middleware 75 | * 76 | * Returns middleware that will authorise the request using oauth, depends on route param 77 | * if successful it will allow the request to proceed to the next handler 78 | * 79 | * @return {Function} middleware 80 | */ 81 | OAuth2Server.prototype.checkAuthorise = function (routeParam) { 82 | var self = this; 83 | 84 | return function (req, res, next) { 85 | 86 | if(req.route && req.route[routeParam]) { 87 | new Authorise(self, req, next); 88 | } else { 89 | next(); 90 | } 91 | }; 92 | }; 93 | 94 | /** 95 | * Grant Middleware 96 | * 97 | * Returns middleware that will grant tokens to valid requests. 98 | * This would normally be mounted at '/oauth/token' e.g. 99 | * 100 | * `server.post('/oauth/token', server.oauth.grant());` 101 | * 102 | * @return {Function} middleware 103 | */ 104 | OAuth2Server.prototype.grant = function () { 105 | var self = this; 106 | 107 | return function (req, res, next) { 108 | new Grant(self, req, res, next); 109 | }; 110 | }; 111 | 112 | /** 113 | * Code Auth Grant Middleware 114 | * 115 | * @param {Function} check Function will be called with req to check if the 116 | * user has authorised the request. 117 | * @return {Function} middleware 118 | */ 119 | OAuth2Server.prototype.authCodeGrant = function (check) { 120 | var self = this; 121 | 122 | return function (req, res, next) { 123 | console.log('authCodeGrant'); 124 | new AuthCodeGrant(self, req, res, next, check); 125 | }; 126 | }; 127 | 128 | /** 129 | * Lockdown 130 | * 131 | * When using the lockdown patter, this function should be called after 132 | * all routes have been declared. 133 | * It will search through each route and if it has not been explitly bypassed 134 | * (by passing oauth.bypass) then authorise will be inserted. 135 | * If oauth.grant has been passed it will replace it with the proper grant 136 | * middleware 137 | * NOTE: When using this method, you must PASS the method not CALL the method, 138 | * e.g.: 139 | * 140 | * ` 141 | * server.post('/oauth/token', app.oauth.grant); 142 | * 143 | * server.get('/secrets', function (req, res, next) { 144 | * res.send('secrets'); 145 | * next(); 146 | * }); 147 | * 148 | * server.get('/public', server.oauth.bypass, function (req, res, next) { 149 | * res.send('public'); 150 | * next(); 151 | * }); 152 | * 153 | * server.oauth.lockdown(server); 154 | * ` 155 | * 156 | * @param {Object} server Restify server 157 | */ 158 | OAuth2Server.prototype.lockdown = function (server) { 159 | var self = this; 160 | 161 | //var lockdownRestify = function (mount) { 162 | // //console.log(mount); 163 | //}; 164 | 165 | //var lockdownExpress3 = function (stack) { 166 | // // Check if it's a grant route 167 | // var pos = stack.indexOf(self.grant); 168 | // if (pos !== -1) { 169 | // stack[pos] = self.grant(); 170 | // return; 171 | // } 172 | // 173 | // // Check it's not been explitly bypassed 174 | // pos = stack.indexOf(self.bypass); 175 | // if (pos === -1) { 176 | // stack.unshift(self.authorise()); 177 | // } else { 178 | // stack.splice(pos, 1); 179 | // } 180 | //}; 181 | // 182 | //var lockdownExpress4 = function (layer) { 183 | // if (!layer.route) 184 | // return; 185 | // 186 | // var stack = layer.route.stack; 187 | // var handlers = stack.map(function (item) { 188 | // return item.handle; 189 | // }); 190 | // 191 | // // Check if it's a grant route 192 | // var pos = handlers.indexOf(self.grant); 193 | // if (pos !== -1) { 194 | // stack[pos].handle = self.grant(); 195 | // return; 196 | // } 197 | // 198 | // // Check it's not been explitly bypassed 199 | // pos = handlers.indexOf(self.bypass); 200 | // if (pos === -1) { 201 | // // Add authorise another route (could do it properly with express.route?) 202 | // var copy = {}; 203 | // var first = stack[0]; 204 | // for (var key in first) { 205 | // copy[key] = first[key]; 206 | // } 207 | // copy.handle = self.authorise(); 208 | // stack.unshift(copy); 209 | // } else { 210 | // stack.splice(pos, 1); 211 | // } 212 | //}; 213 | 214 | //for (var i in server.router.mounts) { 215 | // lockdownRestify(server.router.mounts[i]); 216 | //} 217 | }; 218 | 219 | /** 220 | * Bypass 221 | * 222 | * This is used as placeholder for when using the lockdown pattern 223 | * 224 | * @return {Function} noop 225 | */ 226 | OAuth2Server.prototype.bypass = function () {}; 227 | -------------------------------------------------------------------------------- /test/grant.authorization_code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { 27 | model: {}, 28 | grants: ['password', 'refresh_token'] 29 | }); 30 | 31 | app.set('json spaces', 0); 32 | app.use(bodyParser()); 33 | 34 | app.all('/oauth/token', oauth.grant()); 35 | 36 | app.use(oauth.errorHandler()); 37 | 38 | return app; 39 | }; 40 | 41 | describe('Granting with authorization_code grant type', function () { 42 | it('should detect missing parameters', function (done) { 43 | var app = bootstrap({ 44 | model: { 45 | getClient: function (id, secret, callback) { 46 | callback(false, true); 47 | }, 48 | grantTypeAllowed: function (clientId, grantType, callback) { 49 | callback(false, true); 50 | } 51 | }, 52 | grants: ['authorization_code'] 53 | }); 54 | 55 | request(app) 56 | .post('/oauth/token') 57 | .set('Content-Type', 'application/x-www-form-urlencoded') 58 | .send({ 59 | grant_type: 'authorization_code', 60 | client_id: 'thom', 61 | client_secret: 'nightworld' 62 | }) 63 | .expect(400, /no \\"code\\" parameter/i, done); 64 | 65 | }); 66 | 67 | it('should invalid authorization_code', function (done) { 68 | var app = bootstrap({ 69 | model: { 70 | getClient: function (id, secret, callback) { 71 | callback(false, true); 72 | }, 73 | grantTypeAllowed: function (clientId, grantType, callback) { 74 | callback(false, true); 75 | }, 76 | getAuthCode: function (code, callback) { 77 | callback(false); // Fake invalid 78 | } 79 | }, 80 | grants: ['authorization_code'] 81 | }); 82 | 83 | request(app) 84 | .post('/oauth/token') 85 | .set('Content-Type', 'application/x-www-form-urlencoded') 86 | .send({ 87 | grant_type: 'authorization_code', 88 | client_id: 'thom', 89 | client_secret: 'nightworld', 90 | code: 'abc123' 91 | }) 92 | .expect(400, /invalid code/i, done); 93 | }); 94 | 95 | it('should detect invalid client_id', function (done) { 96 | var app = bootstrap({ 97 | model: { 98 | getClient: function (id, secret, callback) { 99 | callback(false, true); 100 | }, 101 | grantTypeAllowed: function (clientId, grantType, callback) { 102 | callback(false, true); 103 | }, 104 | getAuthCode: function (code, callback) { 105 | callback(false, { client_id: 'wrong' }); 106 | } 107 | }, 108 | grants: ['authorization_code'] 109 | }); 110 | 111 | request(app) 112 | .post('/oauth/token') 113 | .set('Content-Type', 'application/x-www-form-urlencoded') 114 | .send({ 115 | grant_type: 'authorization_code', 116 | client_id: 'thom', 117 | client_secret: 'nightworld', 118 | code: 'abc123' 119 | }) 120 | .expect(400, /invalid code/i, done); 121 | }); 122 | 123 | it('should detect expired code', function (done) { 124 | var app = bootstrap({ 125 | model: { 126 | getClient: function (id, secret, callback) { 127 | callback(false, { client_id: 'thom' }); 128 | }, 129 | grantTypeAllowed: function (clientId, grantType, callback) { 130 | callback(false, true); 131 | }, 132 | getAuthCode: function (data, callback) { 133 | callback(false, { 134 | clientId: 'thom', 135 | expires: new Date(+new Date() - 60) 136 | }); 137 | } 138 | }, 139 | grants: ['authorization_code'] 140 | }); 141 | 142 | request(app) 143 | .post('/oauth/token') 144 | .set('Content-Type', 'application/x-www-form-urlencoded') 145 | .send({ 146 | grant_type: 'authorization_code', 147 | client_id: 'thom', 148 | client_secret: 'nightworld', 149 | code: 'abc123' 150 | }) 151 | .expect(400, /code has expired/i, done); 152 | }); 153 | 154 | it('should require code expiration', function (done) { 155 | var app = bootstrap({ 156 | model: { 157 | getClient: function (id, secret, callback) { 158 | callback(false, { client_id: 'thom' }); 159 | }, 160 | grantTypeAllowed: function (clientId, grantType, callback) { 161 | callback(false, true); 162 | }, 163 | getAuthCode: function (data, callback) { 164 | callback(false, { 165 | clientId: 'thom', 166 | expires: null // This is invalid 167 | }); 168 | } 169 | }, 170 | grants: ['authorization_code'] 171 | }); 172 | 173 | request(app) 174 | .post('/oauth/token') 175 | .set('Content-Type', 'application/x-www-form-urlencoded') 176 | .send({ 177 | grant_type: 'authorization_code', 178 | client_id: 'thom', 179 | client_secret: 'nightworld', 180 | code: 'abc123' 181 | }) 182 | .expect(400, /code has expired/i, done); 183 | }); 184 | 185 | 186 | it('should allow valid request', function (done) { 187 | var app = bootstrap({ 188 | model: { 189 | getClient: function (id, secret, callback) { 190 | callback(false, { client_id: 'thom' }); 191 | }, 192 | grantTypeAllowed: function (clientId, grantType, callback) { 193 | callback(false, true); 194 | }, 195 | getAuthCode: function (refreshToken, callback) { 196 | refreshToken.should.equal('abc123'); 197 | callback(false, { 198 | clientId: 'thom', 199 | expires: new Date(), 200 | userId: '123' 201 | }); 202 | }, 203 | saveAccessToken: function (token, clientId, expires, user, cb) { 204 | cb(); 205 | }, 206 | saveRefreshToken: function (data, cb) { 207 | cb(); 208 | }, 209 | expireRefreshToken: function (refreshToken, callback) { 210 | callback(); 211 | } 212 | }, 213 | grants: ['authorization_code'] 214 | }); 215 | 216 | request(app) 217 | .post('/oauth/token') 218 | .set('Content-Type', 'application/x-www-form-urlencoded') 219 | .send({ 220 | grant_type: 'authorization_code', 221 | client_id: 'thom', 222 | client_secret: 'nightworld', 223 | code: 'abc123' 224 | }) 225 | .expect(200, /"access_token":"(.*)"/i, done); 226 | }); 227 | 228 | }); 229 | -------------------------------------------------------------------------------- /test/authorise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | if (oauthConfig === 'mockValid') { 26 | oauthConfig = { 27 | model: { 28 | getAccessToken: function (token, callback) { 29 | token.should.equal('thom'); 30 | var expires = new Date(); 31 | expires.setSeconds(expires.getSeconds() + 20); 32 | callback(false, { expires: expires }); 33 | } 34 | } 35 | }; 36 | } 37 | 38 | var app = express(); 39 | app.oauth = oauth2server(oauthConfig || { model: {} }); 40 | 41 | app.use(bodyParser()); 42 | app.all('/', app.oauth.authorise()); 43 | 44 | 45 | app.all('/', function (req, res) { 46 | res.send('nightworld'); 47 | }); 48 | 49 | app.use(app.oauth.errorHandler()); 50 | 51 | return app; 52 | }; 53 | 54 | describe('Authorise', function() { 55 | 56 | it('should detect no access token', function (done) { 57 | var app = bootstrap('mockValid'); 58 | 59 | request(app) 60 | .get('/') 61 | .expect(400, /the access token was not found/i, done); 62 | }); 63 | 64 | it('should allow valid token as query param', function (done){ 65 | var app = bootstrap('mockValid'); 66 | 67 | request(app) 68 | .get('/?access_token=thom') 69 | .expect(200, /nightworld/, done); 70 | }); 71 | 72 | it('should require application/x-www-form-urlencoded when access token is ' + 73 | 'in body', function (done) { 74 | var app = bootstrap('mockValid'); 75 | 76 | request(app) 77 | .post('/') 78 | .send({ access_token: 'thom' }) 79 | .expect(400, /content type must be application\/x-www-form-urlencoded/i, 80 | done); 81 | }); 82 | 83 | it('should not allow GET when access token in body', function (done) { 84 | var app = bootstrap('mockValid'); 85 | 86 | request(app) 87 | .get('/') 88 | .set('Content-Type', 'application/x-www-form-urlencoded') 89 | .send({ access_token: 'thom' }) 90 | .expect(400, /method cannot be GET/i, done); 91 | }); 92 | 93 | it('should allow valid token in body', function (done){ 94 | var app = bootstrap('mockValid'); 95 | 96 | request(app) 97 | .post('/') 98 | .set('Content-Type', 'application/x-www-form-urlencoded') 99 | .send({ access_token: 'thom' }) 100 | .expect(200, /nightworld/, done); 101 | }); 102 | 103 | it('should detect malformed header', function (done) { 104 | var app = bootstrap('mockValid'); 105 | 106 | request(app) 107 | .get('/') 108 | .set('Authorization', 'Invalid') 109 | .expect(400, /malformed auth header/i, done); 110 | }); 111 | 112 | it('should allow valid token in header', function (done){ 113 | var app = bootstrap('mockValid'); 114 | 115 | request(app) 116 | .get('/') 117 | .set('Authorization', 'Bearer thom') 118 | .expect(200, /nightworld/, done); 119 | }); 120 | 121 | it('should allow exactly one method (get: query + auth)', function (done) { 122 | var app = bootstrap('mockValid'); 123 | 124 | request(app) 125 | .get('/?access_token=thom') 126 | .set('Authorization', 'Invalid') 127 | .expect(400, /only one method may be used/i, done); 128 | }); 129 | 130 | it('should allow exactly one method (post: query + body)', function (done) { 131 | var app = bootstrap('mockValid'); 132 | 133 | request(app) 134 | .post('/?access_token=thom') 135 | .send({ 136 | access_token: 'thom' 137 | }) 138 | .expect(400, /only one method may be used/i, done); 139 | }); 140 | 141 | it('should allow exactly one method (post: query + empty body)', function (done) { 142 | var app = bootstrap('mockValid'); 143 | 144 | request(app) 145 | .post('/?access_token=thom') 146 | .send({ 147 | access_token: '' 148 | }) 149 | .expect(400, /only one method may be used/i, done); 150 | }); 151 | 152 | it('should detect expired token', function (done){ 153 | var app = bootstrap({ 154 | model: { 155 | getAccessToken: function (token, callback) { 156 | callback(false, { expires: 0 }); // Fake expires 157 | } 158 | } 159 | }); 160 | 161 | request(app) 162 | .get('/?access_token=thom') 163 | .expect(401, /the access token provided has expired/i, done); 164 | }); 165 | 166 | it('should passthrough with valid, non-expiring token (token = null)', 167 | function (done) { 168 | var app = bootstrap({ 169 | model: { 170 | getAccessToken: function (token, callback) { 171 | token.should.equal('thom'); 172 | callback(false, { expires: null }); 173 | } 174 | } 175 | }, false); 176 | 177 | app.get('/', app.oauth.authorise(), function (req, res) { 178 | res.send('nightworld'); 179 | }); 180 | 181 | app.use(app.oauth.errorHandler()); 182 | 183 | request(app) 184 | .get('/?access_token=thom') 185 | .expect(200, /nightworld/, done); 186 | }); 187 | 188 | it('should expose the user id when setting userId', function (done) { 189 | var app = bootstrap({ 190 | model: { 191 | getAccessToken: function (token, callback) { 192 | var expires = new Date(); 193 | expires.setSeconds(expires.getSeconds() + 20); 194 | callback(false, { expires: expires , userId: 1 }); 195 | } 196 | } 197 | }, false); 198 | 199 | app.get('/', app.oauth.authorise(), function (req, res) { 200 | req.should.have.property('user'); 201 | req.user.should.have.property('id'); 202 | req.user.id.should.equal(1); 203 | res.send('nightworld'); 204 | }); 205 | 206 | app.use(app.oauth.errorHandler()); 207 | 208 | request(app) 209 | .get('/?access_token=thom') 210 | .expect(200, /nightworld/, done); 211 | }); 212 | 213 | it('should expose the user id when setting user object', function (done) { 214 | var app = bootstrap({ 215 | model: { 216 | getAccessToken: function (token, callback) { 217 | var expires = new Date(); 218 | expires.setSeconds(expires.getSeconds() + 20); 219 | callback(false, { expires: expires , user: { id: 1, name: 'thom' }}); 220 | } 221 | } 222 | }, false); 223 | 224 | app.get('/', app.oauth.authorise(), function (req, res) { 225 | req.should.have.property('user'); 226 | req.user.should.have.property('id'); 227 | req.user.id.should.equal(1); 228 | req.user.should.have.property('name'); 229 | req.user.name.should.equal('thom'); 230 | res.send('nightworld'); 231 | }); 232 | 233 | app.use(app.oauth.errorHandler()); 234 | 235 | request(app) 236 | .get('/?access_token=thom') 237 | .expect(200, /nightworld/, done); 238 | }); 239 | 240 | }); -------------------------------------------------------------------------------- /test/grant.refresh_token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { 27 | model: {}, 28 | grants: ['password', 'refresh_token'] 29 | }); 30 | 31 | app.set('json spaces', 0); 32 | app.use(bodyParser()); 33 | 34 | app.all('/oauth/token', oauth.grant()); 35 | 36 | app.use(oauth.errorHandler()); 37 | 38 | return app; 39 | }; 40 | 41 | describe('Granting with refresh_token grant type', function () { 42 | it('should detect missing refresh_token parameter', function (done) { 43 | var app = bootstrap({ 44 | model: { 45 | getClient: function (id, secret, callback) { 46 | callback(false, true); 47 | }, 48 | grantTypeAllowed: function (clientId, grantType, callback) { 49 | callback(false, true); 50 | } 51 | }, 52 | grants: ['password', 'refresh_token'] 53 | }); 54 | 55 | request(app) 56 | .post('/oauth/token') 57 | .set('Content-Type', 'application/x-www-form-urlencoded') 58 | .send({ 59 | grant_type: 'refresh_token', 60 | client_id: 'thom', 61 | client_secret: 'nightworld' 62 | }) 63 | .expect(400, /no \\"refresh_token\\" parameter/i, done); 64 | 65 | }); 66 | 67 | it('should detect invalid refresh_token', function (done) { 68 | var app = bootstrap({ 69 | model: { 70 | getClient: function (id, secret, callback) { 71 | callback(false, true); 72 | }, 73 | grantTypeAllowed: function (clientId, grantType, callback) { 74 | callback(false, true); 75 | }, 76 | getRefreshToken: function (data, callback) { 77 | callback(false, false); 78 | } 79 | }, 80 | grants: ['password', 'refresh_token'] 81 | }); 82 | 83 | request(app) 84 | .post('/oauth/token') 85 | .set('Content-Type', 'application/x-www-form-urlencoded') 86 | .send({ 87 | grant_type: 'refresh_token', 88 | client_id: 'thom', 89 | client_secret: 'nightworld', 90 | refresh_token: 'abc123' 91 | }) 92 | .expect(400, /invalid refresh token/i, done); 93 | 94 | }); 95 | 96 | it('should detect wrong client id', function (done) { 97 | var app = bootstrap({ 98 | model: { 99 | getClient: function (id, secret, callback) { 100 | callback(false, true); 101 | }, 102 | grantTypeAllowed: function (clientId, grantType, callback) { 103 | callback(false, true); 104 | }, 105 | getRefreshToken: function (data, callback) { 106 | callback(false, { client_id: 'kate' }); 107 | } 108 | }, 109 | grants: ['password', 'refresh_token'] 110 | }); 111 | 112 | request(app) 113 | .post('/oauth/token') 114 | .set('Content-Type', 'application/x-www-form-urlencoded') 115 | .send({ 116 | grant_type: 'refresh_token', 117 | client_id: 'thom', 118 | client_secret: 'nightworld', 119 | refresh_token: 'abc123' 120 | }) 121 | .expect(400, /invalid refresh token/i, done); 122 | 123 | }); 124 | 125 | it('should detect expired refresh token', function (done) { 126 | var app = bootstrap({ 127 | model: { 128 | getClient: function (id, secret, callback) { 129 | callback(false, { clientId: 'thom' }); 130 | }, 131 | grantTypeAllowed: function (clientId, grantType, callback) { 132 | callback(false, true); 133 | }, 134 | getRefreshToken: function (data, callback) { 135 | callback(false, { 136 | clientId: 'thom', 137 | expires: new Date(+new Date() - 60) 138 | }); 139 | } 140 | }, 141 | grants: ['password', 'refresh_token'] 142 | }); 143 | 144 | request(app) 145 | .post('/oauth/token') 146 | .set('Content-Type', 'application/x-www-form-urlencoded') 147 | .send({ 148 | grant_type: 'refresh_token', 149 | client_id: 'thom', 150 | client_secret: 'nightworld', 151 | refresh_token: 'abc123' 152 | }) 153 | .expect(400, /refresh token has expired/i, done); 154 | 155 | }); 156 | 157 | it('should allow valid request', function (done) { 158 | var app = bootstrap({ 159 | model: { 160 | getClient: function (id, secret, callback) { 161 | callback(false, { clientId: 'thom' }); 162 | }, 163 | grantTypeAllowed: function (clientId, grantType, callback) { 164 | callback(false, true); 165 | }, 166 | getRefreshToken: function (refreshToken, callback) { 167 | refreshToken.should.equal('abc123'); 168 | callback(false, { 169 | clientId: 'thom', 170 | expires: new Date(), 171 | userId: '123' 172 | }); 173 | }, 174 | saveAccessToken: function (token, clientId, expires, user, cb) { 175 | cb(); 176 | }, 177 | saveRefreshToken: function (token, clientId, expires, user, cb) { 178 | cb(); 179 | }, 180 | expireRefreshToken: function (refreshToken, callback) { 181 | callback(); 182 | } 183 | }, 184 | grants: ['password', 'refresh_token'] 185 | }); 186 | 187 | request(app) 188 | .post('/oauth/token') 189 | .set('Content-Type', 'application/x-www-form-urlencoded') 190 | .send({ 191 | grant_type: 'refresh_token', 192 | client_id: 'thom', 193 | client_secret: 'nightworld', 194 | refresh_token: 'abc123' 195 | }) 196 | .expect(200, /"access_token":"(.*)",(.*)"refresh_token":"(.*)"/i, done); 197 | 198 | }); 199 | 200 | it('should allow valid request with user object', function (done) { 201 | var app = bootstrap({ 202 | model: { 203 | getClient: function (id, secret, callback) { 204 | callback(false, { clientId: 'thom' }); 205 | }, 206 | grantTypeAllowed: function (clientId, grantType, callback) { 207 | callback(false, true); 208 | }, 209 | getRefreshToken: function (refreshToken, callback) { 210 | refreshToken.should.equal('abc123'); 211 | callback(false, { 212 | clientId: 'thom', 213 | expires: new Date(), 214 | user: { 215 | id: '123' 216 | } 217 | }); 218 | }, 219 | saveAccessToken: function (token, clientId, expires, user, cb) { 220 | cb(); 221 | }, 222 | saveRefreshToken: function (token, clientId, expires, user, cb) { 223 | cb(); 224 | }, 225 | expireRefreshToken: function (refreshToken, callback) { 226 | callback(); 227 | } 228 | }, 229 | grants: ['password', 'refresh_token'] 230 | }); 231 | 232 | request(app) 233 | .post('/oauth/token') 234 | .set('Content-Type', 'application/x-www-form-urlencoded') 235 | .send({ 236 | grant_type: 'refresh_token', 237 | client_id: 'thom', 238 | client_secret: 'nightworld', 239 | refresh_token: 'abc123' 240 | }) 241 | .expect(200, /"access_token":"(.*)",(.*)"refresh_token":"(.*)"/i, done); 242 | 243 | }); 244 | 245 | it('should allow valid request with non-expiring token (token= null)', function (done) { 246 | var app = bootstrap({ 247 | model: { 248 | getClient: function (id, secret, callback) { 249 | callback(false, { clientId: 'thom' }); 250 | }, 251 | grantTypeAllowed: function (clientId, grantType, callback) { 252 | callback(false, true); 253 | }, 254 | getRefreshToken: function (data, callback) { 255 | callback(false, { 256 | clientId: 'thom', 257 | expires: null, 258 | userId: '123' 259 | }); 260 | }, 261 | saveAccessToken: function (token, clientId, expires, user, cb) { 262 | cb(); 263 | }, 264 | saveRefreshToken: function (token, clientId, expires, user, cb) { 265 | cb(); 266 | }, 267 | expireRefreshToken: function (refreshToken, callback) { 268 | callback(); 269 | } 270 | }, 271 | grants: ['password', 'refresh_token'] 272 | }); 273 | 274 | request(app) 275 | .post('/oauth/token') 276 | .set('Content-Type', 'application/x-www-form-urlencoded') 277 | .send({ 278 | grant_type: 'refresh_token', 279 | client_id: 'thom', 280 | client_secret: 'nightworld', 281 | refresh_token: 'abc123' 282 | }) 283 | .expect(200, /"access_token":"(.*)",(.*)"refresh_token":"(.*)"/i, done); 284 | 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /test/authCodeGrant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (model, params, continueAfterResponse) { 25 | 26 | var app = express(); 27 | app.oauth = oauth2server({ 28 | model: model || {}, 29 | continueAfterResponse: continueAfterResponse 30 | }); 31 | 32 | app.use(bodyParser()); 33 | 34 | app.post('/authorise', app.oauth.authCodeGrant(function (req, next) { 35 | next.apply(null, params || []); 36 | })); 37 | 38 | app.get('/authorise', app.oauth.authCodeGrant(function (req, next) { 39 | next.apply(null, params || []); 40 | })); 41 | 42 | app.use(app.oauth.errorHandler()); 43 | 44 | return app; 45 | }; 46 | 47 | describe('AuthCodeGrant', function() { 48 | 49 | it('should detect no response type', function (done) { 50 | var app = bootstrap(); 51 | 52 | request(app) 53 | .post('/authorise') 54 | .expect(400, /invalid response_type parameter/i, done); 55 | }); 56 | 57 | it('should detect invalid response type', function (done) { 58 | var app = bootstrap(); 59 | 60 | request(app) 61 | .post('/authorise') 62 | .send({ response_type: 'token' }) 63 | .expect(400, /invalid response_type parameter/i, done); 64 | }); 65 | 66 | it('should detect no client_id', function (done) { 67 | var app = bootstrap(); 68 | 69 | request(app) 70 | .post('/authorise') 71 | .send({ response_type: 'code' }) 72 | .expect(400, /invalid or missing client_id parameter/i, done); 73 | }); 74 | 75 | it('should detect no redirect_uri', function (done) { 76 | var app = bootstrap(); 77 | 78 | request(app) 79 | .post('/authorise') 80 | .send({ 81 | response_type: 'code', 82 | client_id: 'thom' 83 | }) 84 | .expect(400, /invalid or missing redirect_uri parameter/i, done); 85 | }); 86 | 87 | it('should detect invalid client', function (done) { 88 | var app = bootstrap({ 89 | getClient: function (clientId, clientSecret, callback) { 90 | callback(); // Fake invalid 91 | } 92 | }); 93 | 94 | request(app) 95 | .post('/authorise') 96 | .send({ 97 | response_type: 'code', 98 | client_id: 'thom', 99 | redirect_uri: 'http://nightworld.com' 100 | }) 101 | .expect('WWW-Authenticate', 'Basic realm="Service"') 102 | .expect(400, /invalid client credentials/i, done); 103 | }); 104 | 105 | it('should detect mismatching redirect_uri with a string', function (done) { 106 | var app = bootstrap({ 107 | getClient: function (clientId, clientSecret, callback) { 108 | callback(false, { 109 | clientId: 'thom', 110 | redirectUri: 'http://nightworld.com' 111 | }); 112 | } 113 | }); 114 | 115 | request(app) 116 | .post('/authorise') 117 | .send({ 118 | response_type: 'code', 119 | client_id: 'thom', 120 | redirect_uri: 'http://wrong.com' 121 | }) 122 | .expect(400, /redirect_uri does not match/i, done); 123 | }); 124 | 125 | it('should detect mismatching redirect_uri within an array', function (done) { 126 | var app = bootstrap({ 127 | getClient: function (clientId, clientSecret, callback) { 128 | callback(false, { 129 | clientId: 'thom', 130 | redirectUri: ['http://nightworld.com','http://dayworld.com'] 131 | }); 132 | } 133 | }); 134 | 135 | request(app) 136 | .post('/authorise') 137 | .send({ 138 | response_type: 'code', 139 | client_id: 'thom', 140 | redirect_uri: 'http://wrong.com' 141 | }) 142 | .expect(400, /redirect_uri does not match/i, done); 143 | }); 144 | 145 | it('should accept a valid redirect_uri within an array', function (done) { 146 | var app = bootstrap({ 147 | getClient: function (clientId, clientSecret, callback) { 148 | callback(false, { 149 | clientId: 'thom', 150 | redirectUri: ['http://nightworld.com','http://dayworld.com'] 151 | }); 152 | } 153 | }); 154 | 155 | request(app) 156 | .post('/authorise') 157 | .send({ 158 | response_type: 'code', 159 | client_id: 'thom', 160 | redirect_uri: 'http://nightworld.com' 161 | }) 162 | .expect(302, /Moved temporarily/i, done); 163 | }); 164 | 165 | it('should accept a valid redirect_uri with a string', function (done) { 166 | var app = bootstrap({ 167 | getClient: function (clientId, clientSecret, callback) { 168 | callback(false, { 169 | clientId: 'thom', 170 | redirectUri: 'http://nightworld.com' 171 | }); 172 | } 173 | }); 174 | 175 | request(app) 176 | .post('/authorise') 177 | .send({ 178 | response_type: 'code', 179 | client_id: 'thom', 180 | redirect_uri: 'http://nightworld.com' 181 | }) 182 | .expect(302, /Moved temporarily/i, done); 183 | }); 184 | 185 | it('should detect user access denied', function (done) { 186 | var app = bootstrap({ 187 | getClient: function (clientId, clientSecret, callback) { 188 | callback(false, { 189 | clientId: 'thom', 190 | redirectUri: 'http://nightworld.com' 191 | }); 192 | } 193 | }, [false, false]); 194 | 195 | request(app) 196 | .post('/authorise') 197 | .send({ 198 | response_type: 'code', 199 | client_id: 'thom', 200 | redirect_uri: 'http://nightworld.com' 201 | }) 202 | .expect(302, 203 | /Redirecting to http:\/\/nightworld.com\?error=access_denied/i, done); 204 | }); 205 | 206 | it('should try to save auth code', function (done) { 207 | var app = bootstrap({ 208 | getClient: function (clientId, clientSecret, callback) { 209 | callback(false, { 210 | clientId: 'thom', 211 | redirectUri: 'http://nightworld.com' 212 | }); 213 | }, 214 | saveAuthCode: function (authCode, clientId, expires, user, callback) { 215 | should.exist(authCode); 216 | authCode.should.have.lengthOf(40); 217 | clientId.should.equal('thom'); 218 | (+expires).should.be.within(2, (+new Date()) + 30000); 219 | done(); 220 | } 221 | }, [false, true]); 222 | 223 | request(app) 224 | .post('/authorise') 225 | .send({ 226 | response_type: 'code', 227 | client_id: 'thom', 228 | redirect_uri: 'http://nightworld.com' 229 | }) 230 | .end(); 231 | }); 232 | 233 | it('should accept valid request and return code using POST', function (done) { 234 | var code; 235 | 236 | var app = bootstrap({ 237 | getClient: function (clientId, clientSecret, callback) { 238 | callback(false, { 239 | clientId: 'thom', 240 | redirectUri: 'http://nightworld.com' 241 | }); 242 | }, 243 | saveAuthCode: function (authCode, clientId, expires, user, callback) { 244 | should.exist(authCode); 245 | code = authCode; 246 | callback(); 247 | } 248 | }, [false, true]); 249 | 250 | request(app) 251 | .post('/authorise') 252 | .send({ 253 | response_type: 'code', 254 | client_id: 'thom', 255 | redirect_uri: 'http://nightworld.com' 256 | }) 257 | .expect(302, function (err, res) { 258 | res.header.location.should.equal('http://nightworld.com?code=' + code); 259 | done(); 260 | }); 261 | }); 262 | 263 | it('should accept valid request and return code using GET', function (done) { 264 | var code; 265 | 266 | var app = bootstrap({ 267 | getClient: function (clientId, clientSecret, callback) { 268 | callback(false, { 269 | clientId: 'thom', 270 | redirectUri: 'http://nightworld.com' 271 | }); 272 | }, 273 | saveAuthCode: function (authCode, clientId, expires, user, callback) { 274 | should.exist(authCode); 275 | code = authCode; 276 | callback(); 277 | } 278 | }, [false, true]); 279 | 280 | request(app) 281 | .get('/authorise') 282 | .query({ 283 | response_type: 'code', 284 | client_id: 'thom', 285 | redirect_uri: 'http://nightworld.com' 286 | }) 287 | .expect(302, function (err, res) { 288 | res.header.location.should.equal('http://nightworld.com?code=' + code); 289 | done(); 290 | }); 291 | }); 292 | 293 | it('should accept valid request and return code and state using GET', function (done) { 294 | var code; 295 | 296 | var app = bootstrap({ 297 | getClient: function (clientId, clientSecret, callback) { 298 | callback(false, { 299 | clientId: 'thom', 300 | redirectUri: 'http://nightworld.com' 301 | }); 302 | }, 303 | saveAuthCode: function (authCode, clientId, expires, user, callback) { 304 | should.exist(authCode); 305 | code = authCode; 306 | callback(); 307 | } 308 | }, [false, true]); 309 | 310 | request(app) 311 | .get('/authorise') 312 | .query({ 313 | response_type: 'code', 314 | client_id: 'thom', 315 | redirect_uri: 'http://nightworld.com', 316 | state: 'some_state' 317 | }) 318 | .expect(302, function (err, res) { 319 | res.header.location.should.equal('http://nightworld.com?code=' + code + '&state=some_state'); 320 | done(); 321 | }); 322 | }); 323 | 324 | it('should continue after success response if continueAfterResponse = true', function (done) { 325 | var app = bootstrap({ 326 | getClient: function (clientId, clientSecret, callback) { 327 | callback(false, { 328 | clientId: 'thom', 329 | redirectUri: 'http://nightworld.com' 330 | }); 331 | }, 332 | saveAuthCode: function (authCode, clientId, expires, user, callback) { 333 | callback(); 334 | } 335 | }, [false, true], true); 336 | 337 | var hit = false; 338 | app.all('*', function (req, res, done) { 339 | hit = true; 340 | }); 341 | 342 | request(app) 343 | .post('/authorise') 344 | .send({ 345 | response_type: 'code', 346 | client_id: 'thom', 347 | redirect_uri: 'http://nightworld.com' 348 | }) 349 | .end(function (err, res) { 350 | if (err) return done(err); 351 | hit.should.equal(true); 352 | done(); 353 | }); 354 | }); 355 | 356 | it('should continue after redirect response if continueAfterResponse = true', function (done) { 357 | var app = bootstrap({ 358 | getClient: function (clientId, clientSecret, callback) { 359 | callback(false, { 360 | clientId: 'thom', 361 | redirectUri: 'http://nightworld.com' 362 | }); 363 | } 364 | }, [false, false], true); 365 | 366 | var hit = false; 367 | app.all('*', function (req, res, done) { 368 | hit = true; 369 | }); 370 | 371 | request(app) 372 | .post('/authorise') 373 | .send({ 374 | response_type: 'code', 375 | client_id: 'thom', 376 | redirect_uri: 'http://nightworld.com' 377 | }) 378 | .end(function (err, res) { 379 | if (err) return done(err); 380 | hit.should.equal(true); 381 | done(); 382 | }); 383 | }); 384 | 385 | }); 386 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Node OAuth2 Server 2 | 3 | Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with [resitfy](http://mcavage.me/node-restify/) in [node.js](http://nodejs.org/) 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install oauth2-server 9 | ``` 10 | 11 | ## Quick Start 12 | 13 | The module provides one middleware for authorization and routing, use it as you would any other middleware: 14 | 15 | ```js 16 | var restify = require('restify'), 17 | oauthserver = require('oauth2-server-restify'); 18 | 19 | var server = restify.createServer(); 20 | 21 | server.use(restify.bodyParser({})); // REQUIRED 22 | 23 | server.oauth = oauthserver({ 24 | model: {}, // See below for specification 25 | grants: ['password'], 26 | debug: true 27 | }); 28 | 29 | server.post('/oauth/token', server.oauth.grant()); 30 | 31 | server.get('/', server.oauth.authorise(), function (req, res, next) { 32 | res.send('Secret area'); 33 | next(); 34 | }); 35 | 36 | 37 | server.listen(3000); 38 | ``` 39 | 40 | After running with node, visting http://127.0.0.1:3000 should present you with a json response saying your access token could not be found. 41 | 42 | Note: As no model was actually implemented here, delving any deeper, i.e. passing an access token, will just cause a server error. See below for the specification of what's required from the model. 43 | 44 | ## Features 45 | 46 | - Supports authorization_code, password, refresh_token, client_credentials and extension (custom) grant types 47 | - Implicitly supports any form of storage e.g. PostgreSQL, MySQL, Mongo, Redis... 48 | - Full test suite 49 | 50 | ## Options 51 | 52 | - *string* **model** 53 | - Model object (see below) 54 | - *array* **grants** 55 | - grant types you wish to support, currently the module supports `password` and `refresh_token` 56 | - Default: `[]` 57 | - *function|boolean* **debug** 58 | - If `true` errors will be logged to console. You may also pass a custom function, in which case that function will be called with the error as it's first argument 59 | - Default: `false` 60 | - *number* **accessTokenLifetime** 61 | - Life of access tokens in seconds 62 | - If `null`, tokens will considered to never expire 63 | - Default: `3600` 64 | - *number* **refreshTokenLifetime** 65 | - Life of refresh tokens in seconds 66 | - If `null`, tokens will considered to never expire 67 | - Default: `1209600` 68 | - *number* **authCodeLifetime** 69 | - Life of auth codes in seconds 70 | - Default: `30` 71 | - *regexp* **clientIdRegex** 72 | - Regex to match auth codes against before checking model 73 | - Default: `/^[a-z0-9-_]{3,40}$/i` 74 | - *boolean* **passthroughErrors** 75 | - If true, **non grant** errors will not be handled internally (so you can ensure a consistent format with the rest of your api) 76 | - *boolean* **continueAfterResponse** 77 | - If true, `next` will be called even if a response has been sent (you probably don't want this) 78 | 79 | ## Model Specification 80 | 81 | The module requires a model object through which some aspects or storage, retrieval and custom validation are abstracted. 82 | The last parameter of all methods is a callback of which the first parameter is always used to indicate an error. 83 | 84 | Note: see https://github.com/thomseddon/node-oauth2-server/tree/master/examples/postgresql for a full model example using postgres. 85 | 86 | ### Always Required 87 | 88 | #### getAccessToken (bearerToken, callback) 89 | - *string* **bearerToken** 90 | - The bearer token (access token) that has been provided 91 | - *function* **callback (error, accessToken)** 92 | - *mixed* **error** 93 | - Truthy to indicate an error 94 | - *object* **accessToken** 95 | - The access token retrieved form storage or falsey to indicate invalid access token 96 | - Must contain the following keys: 97 | - *date* **expires** 98 | - The date when it expires 99 | - `null` to indicate the token **never expires** 100 | - *string|number* **userId** 101 | - The user id (saved in req.user.id) 102 | 103 | #### getClient (clientId, clientSecret, callback) 104 | - *string* **clientId** 105 | - *string|null* **clientSecret** 106 | - If null, omit from search query (only search by clientId) 107 | - *function* **callback (error, client)** 108 | - *mixed* **error** 109 | - Truthy to indicate an error 110 | - *object* **client** 111 | - The client retrieved from storage or falsey to indicate an invalid client 112 | - Saved in `req.client` 113 | - Must contain the following keys: 114 | - *string* **clientId** 115 | 116 | #### grantTypeAllowed (clientId, grantType, callback) 117 | - *string* **clientId** 118 | - *string* **grantType** 119 | - *function* **callback (error, allowed)** 120 | - *mixed* **error** 121 | - Truthy to indicate an error 122 | - *boolean* **allowed** 123 | - Indicates whether the grantType is allowed for this clientId 124 | 125 | #### saveAccessToken (accessToken, clientId, expires, user, callback) 126 | - *string* **accessToken** 127 | - *string* **clientId** 128 | - *date* **expires** 129 | - *object* **user** 130 | - *function* **callback (error)** 131 | - *mixed* **error** 132 | - Truthy to indicate an error 133 | 134 | 135 | ### Required for `authorization_code` grant type 136 | 137 | #### getAuthCode (authCode, callback) 138 | - *string* **authCode** 139 | - *function* **callback (error, authCode)** 140 | - *mixed* **error** 141 | - Truthy to indicate an error 142 | - *object* **authCode** 143 | - The authorization code retrieved form storage or falsey to indicate invalid code 144 | - Must contain the following keys: 145 | - *string|number* **clientId** 146 | - client id associated with this auth code 147 | - *date* **expires** 148 | - The date when it expires 149 | - *string|number* **userId** 150 | - The userId 151 | 152 | #### saveAuthCode (authCode, clientId, expires, user, callback) 153 | - *string* **authCode** 154 | - *string* **clientId** 155 | - *date* **expires** 156 | - *mixed* **user** 157 | - Whatever was passed as `user` to the codeGrant function (see example) 158 | - *function* **callback (error)** 159 | - *mixed* **error** 160 | - Truthy to indicate an error 161 | 162 | 163 | ### Required for `password` grant type 164 | 165 | #### getUser (username, password, callback) 166 | - *string* **username** 167 | - *string* **password** 168 | - *function* **callback (error, user)** 169 | - *mixed* **error** 170 | - Truthy to indicate an error 171 | - *object* **user** 172 | - The user retrieved from storage or falsey to indicate an invalid user 173 | - Saved in `req.user` 174 | - Must contain the following keys: 175 | - *string|number* **id** 176 | 177 | ### Required for `refresh_token` grant type 178 | 179 | #### saveRefreshToken (refreshToken, clientId, expires, user, callback) 180 | - *string* **refreshToken** 181 | - *string* **clientId** 182 | - *date* **expires** 183 | - *object* **user** 184 | - *function* **callback (error)** 185 | - *mixed* **error** 186 | - Truthy to indicate an error 187 | 188 | #### getRefreshToken (refreshToken, callback) 189 | - *string* **refreshToken** 190 | - The bearer token (refresh token) that has been provided 191 | - *function* **callback (error, refreshToken)** 192 | - *mixed* **error** 193 | - Truthy to indicate an error 194 | - *object* **refreshToken** 195 | - The refresh token retrieved form storage or falsey to indicate invalid refresh token 196 | - Must contain the following keys: 197 | - *string|number* **clientId** 198 | - client id associated with this token 199 | - *date* **expires** 200 | - The date when it expires 201 | - `null` to indicate the token **never expires** 202 | - *string|number* **userId** 203 | - The userId 204 | 205 | 206 | ### Optional for Refresh Token grant type 207 | 208 | #### revokeRefreshToken (refreshToken, callback) 209 | The spec does not actually require that you revoke the old token - hence this is optional (Last paragraph: http://tools.ietf.org/html/rfc6749#section-6) 210 | - *string* **refreshToken** 211 | - *function* **callback (error)** 212 | - *mixed* **error** 213 | - Truthy to indicate an error 214 | 215 | ### Required for [extension grant](#extension-grants) grant type 216 | 217 | #### extendedGrant (grantType, req, callback) 218 | - *string* **grantType** 219 | - The (custom) grant type 220 | - *object* **req** 221 | - The raw request 222 | - *function* **callback (error, supported, user)** 223 | - *mixed* **error** 224 | - Truthy to indicate an error 225 | - *boolean* **supported** 226 | - Whether you support the grant type 227 | - *object* **user** 228 | - The user retrieved from storage or falsey to indicate an invalid user 229 | - Saved in `req.user` 230 | - Must contain the following keys: 231 | - *string|number* **id** 232 | 233 | ### Required for `client_credentials` grant type 234 | 235 | #### getUserFromClient (clientId, clientSecret, callback) 236 | - *string* **clientId** 237 | - *string* **clientSecret** 238 | - *function* **callback (error, user)** 239 | - *mixed* **error** 240 | - Truthy to indicate an error 241 | - *object* **user** 242 | - The user retrieved from storage or falsey to indicate an invalid user 243 | - Saved in `req.user` 244 | - Must contain the following keys: 245 | - *string|number* **id** 246 | 247 | 248 | ### Optional 249 | 250 | #### generateToken (type, callback) 251 | - *string* **type** 252 | - `accessToken` or `refreshToken` 253 | - *function* **callback (error, token)** 254 | - *mixed* **error** 255 | - Truthy to indicate an error 256 | - *string|object|null* **token** 257 | - *string* indicates success 258 | - *null* indicates to revert to the default token generator 259 | - *object* indicates a reissue (i.e. will not be passed to saveAccessToken/saveRefreshToken) 260 | - Must contain the following keys (if object): 261 | - *string* **accessToken** OR **refreshToken** dependant on type 262 | 263 | ## Extension Grants 264 | You can support extension/custom grants by implementing the extendedGrant method as outlined above. 265 | Any requests that begin with http(s):// (as [defined in the spec](http://tools.ietf.org/html/rfc6749#section-4.5)) will be passed to it for you to handle. 266 | You can access the grant type via the first argument and you should pass back supported as `false` if you do not support it to ensure a consistent (and compliant) response. 267 | 268 | ## Example using the `password` grant type 269 | 270 | First you must insert client id/secret and user into storage. This is out of the scope of this example. 271 | 272 | To obtain a token you should POST to `/oauth/token`. You should include your client credentials in 273 | the Authorization header ("Basic " + client_id:client_secret base64'd), and then grant_type ("password"), 274 | username and password in the request body, for example: 275 | 276 | ``` 277 | POST /oauth/token HTTP/1.1 278 | Host: server.example.com 279 | Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 280 | Content-Type: application/x-www-form-urlencoded 281 | 282 | grant_type=password&username=johndoe&password=A3ddj3w 283 | ``` 284 | This will then call the following on your model (in this order): 285 | - getClient (clientId, clientSecret, callback) 286 | - grantTypeAllowed (clientId, grantType, callback) 287 | - getUser (username, password, callback) 288 | - saveAccessToken (accessToken, clientId, expires, user, callback) 289 | - saveRefreshToken (refreshToken, clientId, expires, user, callback) **(if using)** 290 | 291 | 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): 292 | 293 | ``` 294 | HTTP/1.1 200 OK 295 | Content-Type: application/json;charset=UTF-8 296 | Cache-Control: no-store 297 | Pragma: no-cache 298 | 299 | { 300 | "access_token":"2YotnFZFEjr1zCsicMWpAA", 301 | "token_type":"bearer", 302 | "expires_in":3600, 303 | "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" 304 | } 305 | ``` 306 | 307 | ## Changelog 308 | 309 | See: https://github.com/thomseddon/node-oauth2-server/blob/master/Changelog.md 310 | 311 | ## Credits 312 | 313 | Copyright (c) 2013 Thom Seddon & Marcos Sanz 314 | 315 | ## License 316 | 317 | [Apache, Version 2.0](https://github.com/thomseddon/node-oauth2-server/blob/master/LICENSE) 318 | -------------------------------------------------------------------------------- /lib/grant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var auth = require('basic-auth'), 18 | error = require('node-restify-errors'), 19 | runner = require('./runner'), 20 | token = require('./token'); 21 | 22 | module.exports = Grant; 23 | 24 | /** 25 | * This is the function order used by the runner 26 | * 27 | * @type {Array} 28 | */ 29 | var fns = [ 30 | extractCredentials, 31 | checkClient, 32 | checkGrantTypeAllowed, 33 | checkGrantType, 34 | generateAccessToken, 35 | saveAccessToken, 36 | generateRefreshToken, 37 | saveRefreshToken, 38 | sendResponse 39 | ]; 40 | 41 | /** 42 | * Grant 43 | * 44 | * @param {Object} config Instance of OAuth object 45 | * @param {Object} req 46 | * @param {Object} res 47 | * @param {Function} next 48 | */ 49 | function Grant (config, req, res, next) { 50 | this.config = config; 51 | this.model = config.model; 52 | this.now = new Date(); 53 | this.req = req; 54 | this.res = res; 55 | 56 | runner(fns, this, next); 57 | } 58 | 59 | /** 60 | * Basic request validation and extraction of grant_type and client creds 61 | * 62 | * @param {Function} next 63 | * @this OAuth 64 | */ 65 | function extractCredentials (next) { 66 | // Only POST via application/x-www-form-urlencoded is acceptable 67 | if (this.req.method !== 'POST' || 68 | !this.req.is('application/x-www-form-urlencoded')) { 69 | return next(new error.BadMethodError('Method must be POST with application/x-www-form-urlencoded encoding')); 70 | } 71 | 72 | // Grant type 73 | this.grantType = this.req.body && this.req.body.grant_type; 74 | if (!this.grantType || !this.grantType.match(this.config.regex.grantType)) { 75 | return next(new error.BadMethodError('Invalid or missing grant_type parameter')); 76 | } 77 | 78 | // Extract credentials 79 | // http://tools.ietf.org/html/rfc6749#section-3.2.1 80 | this.client = credsFromBasic(this.req) || credsFromBody(this.req); 81 | 82 | if (!this.client.clientId || 83 | !this.client.clientId.match(this.config.regex.clientId)) { 84 | return next(new error.InvalidCredentialsError('Invalid or missing client_id parameter')); 85 | } else if (!this.client.clientSecret) { 86 | return next(new error.InvalidCredentialsError('Missing client_secret parameter')); 87 | } 88 | 89 | next(); 90 | } 91 | 92 | /** 93 | * Client Object (internal use only) 94 | * 95 | * @param {String} id client_id 96 | * @param {String} secret client_secret 97 | */ 98 | function Client (id, secret) { 99 | this.clientId = id; 100 | this.clientSecret = secret; 101 | } 102 | 103 | /** 104 | * Extract client creds from Basic auth 105 | * 106 | * @return {Object} Client 107 | */ 108 | function credsFromBasic (req) { 109 | var user = auth(req); 110 | 111 | if (!user) return false; 112 | 113 | return new Client(user.name, user.pass); 114 | } 115 | 116 | /** 117 | * Extract client creds from body 118 | * 119 | * @return {Object} Client 120 | */ 121 | function credsFromBody (req) { 122 | return new Client(req.body.client_id, req.body.client_secret); 123 | } 124 | 125 | /** 126 | * Check extracted client against model 127 | * 128 | * @param {Function} next 129 | * @this OAuth 130 | */ 131 | function checkClient (next) { 132 | this.model.getClient(this.client.clientId, this.client.clientSecret, 133 | function (err, client) { 134 | if (err) return next(new error.InternalError(err)); 135 | 136 | if (!client) { 137 | return next(new error.InvalidCredentialsError('Client credentials are invalid')); 138 | } 139 | 140 | next(); 141 | }); 142 | } 143 | 144 | /** 145 | * Delegate to the relvant grant function based on grant_type 146 | * 147 | * @param {Function} next 148 | * @this OAuth 149 | */ 150 | function checkGrantType (next) { 151 | if (this.grantType.match(/^http(s|):\/\//) && this.model.extendedGrant) { 152 | return useExtendedGrant.call(this, next); 153 | } 154 | 155 | switch (this.grantType) { 156 | case 'authorization_code': 157 | return useAuthCodeGrant.call(this, next); 158 | case 'password': 159 | return usePasswordGrant.call(this, next); 160 | case 'refresh_token': 161 | return useRefreshTokenGrant.call(this, next); 162 | case 'client_credentials': 163 | return useClientCredentialsGrant.call(this, next); 164 | default: 165 | next(new error.MissingParameterError('Invalid grant_type parameter or parameter missing')); 166 | } 167 | } 168 | 169 | /** 170 | * Grant for authorization_code grant type 171 | * 172 | * @param {Function} next 173 | */ 174 | function useAuthCodeGrant (next) { 175 | var code = this.req.body.code; 176 | 177 | if (!code) { 178 | return next(new error.MissingParameterError('No "code" parameter')); 179 | } 180 | 181 | var self = this; 182 | this.model.getAuthCode(code, function (err, authCode) { 183 | if (err) return next(new error('server_error', false, err)); 184 | 185 | if (!authCode || authCode.clientId !== self.client.clientId) { 186 | return next(new error.InvalidContentError('Invalid code')); 187 | } else if (authCode.expires < self.now) { 188 | return next(new error.InvalidContentError('Code has expired')); 189 | } 190 | 191 | self.user = authCode.user || { id: authCode.userId }; 192 | if (!self.user.id) { 193 | return next(new error.InternalError('No user/userId parameter returned from getauthCode')); 194 | } 195 | 196 | next(); 197 | }); 198 | } 199 | 200 | /** 201 | * Grant for password grant type 202 | * 203 | * @param {Function} next 204 | */ 205 | function usePasswordGrant (next) { 206 | // User credentials 207 | var uname = this.req.body.username, 208 | pword = this.req.body.password; 209 | if (!uname || !pword) { 210 | return next(new error.InvalidCredentialsError('Missing parameters. "username" and "password" are required')); 211 | } 212 | 213 | var self = this; 214 | return this.model.getUser(uname, pword, function (err, user) { 215 | if (err) return next(new error.InternalError(err)); 216 | if (!user) { 217 | return next(new error.InvalidCredentialsError('User credentials are invalid')); 218 | } 219 | 220 | self.user = user; 221 | next(); 222 | }); 223 | } 224 | 225 | /** 226 | * Grant for refresh_token grant type 227 | * 228 | * @param {Function} next 229 | */ 230 | function useRefreshTokenGrant (next) { 231 | var token = this.req.body.refresh_token; 232 | 233 | if (!token) { 234 | return next(new error.BadMethodError('No "refresh_token" parameter')); 235 | } 236 | 237 | var self = this; 238 | this.model.getRefreshToken(token, function (err, refreshToken) { 239 | if (err) return next(new error.InternalError(err)); 240 | 241 | if (!refreshToken || refreshToken.clientId !== self.client.clientId) { 242 | return next(new error.BadMethodError('Invalid refresh token')); 243 | } else if (refreshToken.expires !== null && 244 | refreshToken.expires < self.now) { 245 | return next(new error.BadMethodError('Refresh token has expired')); 246 | } 247 | 248 | if (!refreshToken.user && !refreshToken.userId) { 249 | return next(new error.InternalError('No user/userId parameter returned from getRefreshToken')); 250 | } 251 | 252 | self.user = refreshToken.user || { id: refreshToken.userId }; 253 | 254 | if (self.model.revokeRefreshToken) { 255 | return self.model.revokeRefreshToken(token, function (err) { 256 | if (err) return next(new error.InternalError(err)); 257 | next(); 258 | }); 259 | } 260 | 261 | next(); 262 | }); 263 | } 264 | 265 | /** 266 | * Grant for client_credentials grant type 267 | * 268 | * @param {Function} next 269 | */ 270 | function useClientCredentialsGrant (next) { 271 | // Client credentials 272 | var clientId = this.client.clientId, 273 | clientSecret = this.client.clientSecret; 274 | 275 | if (!clientId || !clientSecret) { 276 | return next(new error.InvalidCredentialsError('Missing parameters. "client_id" and "client_secret" are required')); 277 | } 278 | 279 | var self = this; 280 | return this.model.getUserFromClient(clientId, clientSecret, 281 | function (err, user) { 282 | if (err) return next(new error.InternalError(err)); 283 | if (!user) { 284 | return next(new error.InvalidCredentialsError('Client credentials are invalid')); 285 | } 286 | 287 | self.user = user; 288 | next(); 289 | }); 290 | } 291 | 292 | /** 293 | * Grant for extended (http://*) grant type 294 | * 295 | * @param {Function} next 296 | */ 297 | function useExtendedGrant (next) { 298 | var self = this; 299 | this.model.extendedGrant(this.grantType, this.req, 300 | function (err, supported, user) { 301 | if (err) { 302 | return next(new error(err.error || 'server_error', 303 | err.description || err.message, err)); 304 | } 305 | 306 | if (!supported) { 307 | return next(new error.BadMethodError('Invalid grant_type parameter or parameter missing')); 308 | } else if (!user || user.id === undefined) { 309 | return next(new error.BadMethodError('Invalid request.')); 310 | } 311 | 312 | self.user = user; 313 | next(); 314 | }); 315 | } 316 | 317 | /** 318 | * Check the grant type is allowed for this client 319 | * 320 | * @param {Function} next 321 | * @this OAuth 322 | */ 323 | function checkGrantTypeAllowed (next) { 324 | this.model.grantTypeAllowed(this.client.clientId, this.grantType, 325 | function (err, allowed) { 326 | if (err) return next(new error.InternalError(err)); 327 | 328 | if (!allowed) { 329 | return next(new error.InvalidCredentialsError('The grant type is unauthorised for this client_id')); 330 | } 331 | 332 | next(); 333 | }); 334 | } 335 | 336 | /** 337 | * Generate an access token 338 | * 339 | * @param {Function} next 340 | * @this OAuth 341 | */ 342 | function generateAccessToken (next) { 343 | var self = this; 344 | token(this, 'accessToken', function (err, token) { 345 | self.accessToken = token; 346 | next(err); 347 | }); 348 | } 349 | 350 | /** 351 | * Save access token with model 352 | * 353 | * @param {Function} next 354 | * @this OAuth 355 | */ 356 | function saveAccessToken (next) { 357 | var accessToken = this.accessToken; 358 | 359 | // Object indicates a reissue 360 | if (typeof accessToken === 'object' && accessToken.accessToken) { 361 | this.accessToken = accessToken.accessToken; 362 | return next(); 363 | } 364 | 365 | var expires = null; 366 | if (this.config.accessTokenLifetime !== null) { 367 | expires = new Date(this.now); 368 | expires.setSeconds(expires.getSeconds() + this.config.accessTokenLifetime); 369 | } 370 | 371 | this.model.saveAccessToken(accessToken, this.client.clientId, expires, 372 | this.user, function (err, token) { 373 | if (err) return next(new error.InternalError(err)); 374 | next(); 375 | }); 376 | } 377 | 378 | /** 379 | * Generate a refresh token 380 | * 381 | * @param {Function} next 382 | * @this OAuth 383 | */ 384 | function generateRefreshToken (next) { 385 | if (this.config.grants.indexOf('refresh_token') === -1) return next(); 386 | 387 | var self = this; 388 | token(this, 'refreshToken', function (err, token) { 389 | self.refreshToken = token; 390 | next(err); 391 | }); 392 | } 393 | 394 | /** 395 | * Save refresh token with model 396 | * 397 | * @param {Function} next 398 | * @this OAuth 399 | */ 400 | function saveRefreshToken (next) { 401 | var refreshToken = this.refreshToken; 402 | 403 | if (!refreshToken) return next(); 404 | 405 | // Object indicates a reissue 406 | if (typeof refreshToken === 'object' && refreshToken.refreshToken) { 407 | this.refreshToken = refreshToken.refreshToken; 408 | return next(); 409 | } 410 | 411 | var expires = null; 412 | if (this.config.refreshTokenLifetime !== null) { 413 | expires = new Date(this.now); 414 | expires.setSeconds(expires.getSeconds() + this.config.refreshTokenLifetime); 415 | } 416 | 417 | this.model.saveRefreshToken(refreshToken, this.client.clientId, expires, 418 | this.user, function (err, user) { 419 | if (err) return next(new error.InternalError(err)); 420 | next(); 421 | }); 422 | } 423 | 424 | /** 425 | * Create an access token and save it with the model 426 | * 427 | * @param {Function} next 428 | * @this OAuth 429 | */ 430 | function sendResponse (next) { 431 | var response = { 432 | token_type: 'bearer', 433 | access_token: this.accessToken 434 | }; 435 | 436 | if (this.config.accessTokenLifetime !== null) { 437 | response.expires_in = this.config.accessTokenLifetime; 438 | } 439 | 440 | if (this.refreshToken) response.refresh_token = this.refreshToken; 441 | 442 | this.res 443 | .set('Cache-Control', 'no-store') 444 | .set('Pragma', 'no-cache') 445 | .send(response); 446 | 447 | if (this.config.continueAfterResponse) 448 | next(); 449 | } 450 | -------------------------------------------------------------------------------- /test/grant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-present NightWorld. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var express = require('express'), 18 | bodyParser = require('body-parser'), 19 | request = require('supertest'), 20 | should = require('should'); 21 | 22 | var oauth2server = require('../'); 23 | 24 | var bootstrap = function (oauthConfig) { 25 | var app = express(), 26 | oauth = oauth2server(oauthConfig || { 27 | model: {}, 28 | grants: ['password', 'refresh_token'] 29 | }); 30 | 31 | app.set('json spaces', 0); 32 | app.use(bodyParser()); 33 | 34 | app.all('/oauth/token', oauth.grant()); 35 | 36 | app.use(oauth.errorHandler()); 37 | 38 | return app; 39 | }; 40 | 41 | var validBody = { 42 | grant_type: 'password', 43 | client_id: 'thom', 44 | client_secret: 'nightworld', 45 | username: 'thomseddon', 46 | password: 'nightworld' 47 | }; 48 | 49 | describe('Grant', function() { 50 | 51 | describe('when parsing request', function () { 52 | it('should only allow post', function (done) { 53 | var app = bootstrap(); 54 | 55 | request(app) 56 | .get('/oauth/token') 57 | .expect(400, /method must be POST/i, done); 58 | }); 59 | 60 | it('should only allow application/x-www-form-urlencoded', function (done) { 61 | var app = bootstrap(); 62 | 63 | request(app) 64 | .post('/oauth/token') 65 | .set('Content-Type', 'application/json') 66 | .send({}) // Required to be valid JSON 67 | .expect(400, /application\/x-www-form-urlencoded/i, done); 68 | }); 69 | 70 | it('should check grant_type exists', function (done) { 71 | var app = bootstrap(); 72 | 73 | request(app) 74 | .post('/oauth/token') 75 | .set('Content-Type', 'application/x-www-form-urlencoded') 76 | .expect(400, /invalid or missing grant_type parameter/i, done); 77 | }); 78 | 79 | it('should ensure grant_type is allowed', function (done) { 80 | var app = bootstrap({ model: {}, grants: ['refresh_token'] }); 81 | 82 | request(app) 83 | .post('/oauth/token') 84 | .set('Content-Type', 'application/x-www-form-urlencoded') 85 | .send({ grant_type: 'password' }) 86 | .expect(400, /invalid or missing grant_type parameter/i, done); 87 | }); 88 | 89 | it('should check client_id exists', function (done) { 90 | var app = bootstrap(); 91 | 92 | request(app) 93 | .post('/oauth/token') 94 | .set('Content-Type', 'application/x-www-form-urlencoded') 95 | .send({ grant_type: 'password' }) 96 | .expect(400, /invalid or missing client_id parameter/i, done); 97 | }); 98 | 99 | it('should check client_id matches regex', function (done) { 100 | var app = bootstrap({ 101 | clientIdRegex: /match/, 102 | model: {}, 103 | grants: ['password', 'refresh_token'] 104 | }); 105 | 106 | request(app) 107 | .post('/oauth/token') 108 | .set('Content-Type', 'application/x-www-form-urlencoded') 109 | .send({ grant_type: 'password', client_id: 'thom' }) 110 | .expect(400, /invalid or missing client_id parameter/i, done); 111 | }); 112 | 113 | it('should check client_secret exists', function (done) { 114 | var app = bootstrap(); 115 | 116 | request(app) 117 | .post('/oauth/token') 118 | .set('Content-Type', 'application/x-www-form-urlencoded') 119 | .send({ grant_type: 'password', client_id: 'thom' }) 120 | .expect(400, /missing client_secret parameter/i, done); 121 | }); 122 | 123 | it('should extract credentials from body', function (done) { 124 | var app = bootstrap({ 125 | model: { 126 | getClient: function (id, secret, callback) { 127 | id.should.equal('thom'); 128 | secret.should.equal('nightworld'); 129 | callback(false, false); 130 | } 131 | }, 132 | grants: ['password'] 133 | }); 134 | 135 | request(app) 136 | .post('/oauth/token') 137 | .set('Content-Type', 'application/x-www-form-urlencoded') 138 | .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) 139 | .expect(400, done); 140 | }); 141 | 142 | it('should extract credentials from header (Basic)', function (done) { 143 | var app = bootstrap({ 144 | model: { 145 | getClient: function (id, secret, callback) { 146 | id.should.equal('thom'); 147 | secret.should.equal('nightworld'); 148 | callback(false, false); 149 | } 150 | }, 151 | grants: ['password'] 152 | }); 153 | 154 | request(app) 155 | .post('/oauth/token') 156 | .send('grant_type=password&username=test&password=invalid') 157 | .set('Authorization', 'Basic dGhvbTpuaWdodHdvcmxk') 158 | .expect(400, done); 159 | }); 160 | 161 | it('should detect unsupported grant_type', function (done) { 162 | var app = bootstrap({ 163 | model: { 164 | getClient: function (id, secret, callback) { 165 | callback(false, true); 166 | }, 167 | grantTypeAllowed: function (clientId, grantType, callback) { 168 | callback(false, true); 169 | } 170 | }, 171 | grants: ['refresh_token'] 172 | }); 173 | 174 | request(app) 175 | .post('/oauth/token') 176 | .set('Content-Type', 'application/x-www-form-urlencoded') 177 | .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) 178 | .expect(400, /invalid or missing grant_type/i, done); 179 | }); 180 | }); 181 | 182 | describe('check client credentials against model', function () { 183 | it('should detect invalid client', function (done) { 184 | var app = bootstrap({ 185 | model: { 186 | getClient: function (id, secret, callback) { 187 | callback(false, false); // Fake invalid 188 | } 189 | }, 190 | grants: ['password'] 191 | }); 192 | 193 | request(app) 194 | .post('/oauth/token') 195 | .set('Content-Type', 'application/x-www-form-urlencoded') 196 | .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) 197 | .expect(400, /client credentials are invalid/i, done); 198 | }); 199 | }); 200 | 201 | describe('check grant type allowed for client (via model)', function () { 202 | it('should detect grant type not allowed', function (done) { 203 | var app = bootstrap({ 204 | model: { 205 | getClient: function (id, secret, callback) { 206 | callback(false, true); 207 | }, 208 | grantTypeAllowed: function (clientId, grantType, callback) { 209 | callback(false, false); // Not allowed 210 | } 211 | }, 212 | grants: ['password'] 213 | }); 214 | 215 | request(app) 216 | .post('/oauth/token') 217 | .set('Content-Type', 'application/x-www-form-urlencoded') 218 | .send({ grant_type: 'password', client_id: 'thom', client_secret: 'nightworld' }) 219 | .expect(400, /grant type is unauthorised for this client_id/i, done); 220 | }); 221 | }); 222 | 223 | describe('generate access token', function () { 224 | it('should allow override via model', function (done) { 225 | var app = bootstrap({ 226 | model: { 227 | getClient: function (id, secret, callback) { 228 | callback(false, true); 229 | }, 230 | grantTypeAllowed: function (clientId, grantType, callback) { 231 | callback(false, true); 232 | }, 233 | getUser: function (uname, pword, callback) { 234 | callback(false, { id: 1 }); 235 | }, 236 | generateToken: function (type, req, callback) { 237 | callback(false, 'thommy'); 238 | }, 239 | saveAccessToken: function (token, clientId, expires, user, cb) { 240 | token.should.equal('thommy'); 241 | cb(); 242 | } 243 | }, 244 | grants: ['password'] 245 | }); 246 | 247 | request(app) 248 | .post('/oauth/token') 249 | .set('Content-Type', 'application/x-www-form-urlencoded') 250 | .send(validBody) 251 | .expect(200, /thommy/, done); 252 | 253 | }); 254 | 255 | it('should reissue if model returns object', function (done) { 256 | var app = bootstrap({ 257 | model: { 258 | getClient: function (id, secret, callback) { 259 | callback(false, true); 260 | }, 261 | grantTypeAllowed: function (clientId, grantType, callback) { 262 | callback(false, true); 263 | }, 264 | getUser: function (uname, pword, callback) { 265 | callback(false, { id: 1 }); 266 | }, 267 | generateToken: function (type, req, callback) { 268 | callback(false, { accessToken: 'thommy' }); 269 | }, 270 | saveAccessToken: function (token, clientId, expires, user, cb) { 271 | cb(new Error('Should not be saving')); 272 | } 273 | }, 274 | grants: ['password'] 275 | }); 276 | 277 | request(app) 278 | .post('/oauth/token') 279 | .set('Content-Type', 'application/x-www-form-urlencoded') 280 | .send(validBody) 281 | .expect(200, /"access_token":"thommy"/, done); 282 | 283 | }); 284 | }); 285 | 286 | describe('saving access token', function () { 287 | it('should pass valid params to model.saveAccessToken', function (done) { 288 | var app = bootstrap({ 289 | model: { 290 | getClient: function (id, secret, callback) { 291 | callback(false, { client_id: 'thom' }); 292 | }, 293 | grantTypeAllowed: function (clientId, grantType, callback) { 294 | callback(false, true); 295 | }, 296 | getUser: function (uname, pword, callback) { 297 | callback(false, { id: 1 }); 298 | }, 299 | saveAccessToken: function (token, clientId, expires, user, cb) { 300 | token.should.be.instanceOf(String); 301 | token.should.have.length(40); 302 | clientId.should.equal('thom'); 303 | user.id.should.equal(1); 304 | (+expires).should.be.within(10, (+new Date()) + 3600000); 305 | cb(); 306 | } 307 | }, 308 | grants: ['password'] 309 | }); 310 | 311 | request(app) 312 | .post('/oauth/token') 313 | .set('Content-Type', 'application/x-www-form-urlencoded') 314 | .send(validBody) 315 | .expect(200, done); 316 | 317 | }); 318 | 319 | it('should pass valid params to model.saveRefreshToken', function (done) { 320 | var app = bootstrap({ 321 | model: { 322 | getClient: function (id, secret, callback) { 323 | callback(false, { client_id: 'thom' }); 324 | }, 325 | grantTypeAllowed: function (clientId, grantType, callback) { 326 | callback(false, true); 327 | }, 328 | getUser: function (uname, pword, callback) { 329 | callback(false, { id: 1 }); 330 | }, 331 | saveAccessToken: function (token, clientId, expires, user, cb) { 332 | cb(); 333 | }, 334 | saveRefreshToken: function (token, clientId, expires, user, cb) { 335 | token.should.be.instanceOf(String); 336 | token.should.have.length(40); 337 | clientId.should.equal('thom'); 338 | user.id.should.equal(1); 339 | (+expires).should.be.within(10, (+new Date()) + 1209600000); 340 | cb(); 341 | } 342 | }, 343 | grants: ['password', 'refresh_token'] 344 | }); 345 | 346 | request(app) 347 | .post('/oauth/token') 348 | .set('Content-Type', 'application/x-www-form-urlencoded') 349 | .send(validBody) 350 | .expect(200, done); 351 | 352 | }); 353 | }); 354 | 355 | describe('issue access token', function () { 356 | it('should return an oauth compatible response', function (done) { 357 | var app = bootstrap({ 358 | model: { 359 | getClient: function (id, secret, callback) { 360 | callback(false, { clientId: 'thom' }); 361 | }, 362 | grantTypeAllowed: function (clientId, grantType, callback) { 363 | callback(false, true); 364 | }, 365 | getUser: function (uname, pword, callback) { 366 | callback(false, { id: 1 }); 367 | }, 368 | saveAccessToken: function (token, clientId, expires, user, cb) { 369 | cb(); 370 | } 371 | }, 372 | grants: ['password'] 373 | }); 374 | 375 | request(app) 376 | .post('/oauth/token') 377 | .set('Content-Type', 'application/x-www-form-urlencoded') 378 | .send(validBody) 379 | .expect(200) 380 | .end(function (err, res) { 381 | if (err) return done(err); 382 | 383 | res.body.should.have.keys(['access_token', 'token_type', 'expires_in']); 384 | res.body.access_token.should.be.instanceOf(String); 385 | res.body.access_token.should.have.length(40); 386 | res.body.token_type.should.equal('bearer'); 387 | res.body.expires_in.should.equal(3600); 388 | 389 | done(); 390 | }); 391 | 392 | }); 393 | 394 | it('should return an oauth compatible response with refresh_token', function (done) { 395 | var app = bootstrap({ 396 | model: { 397 | getClient: function (id, secret, callback) { 398 | callback(false, { client_id: 'thom' }); 399 | }, 400 | grantTypeAllowed: function (clientId, grantType, callback) { 401 | callback(false, true); 402 | }, 403 | getUser: function (uname, pword, callback) { 404 | callback(false, { id: 1 }); 405 | }, 406 | saveAccessToken: function (token, clientId, expires, user, cb) { 407 | cb(); 408 | }, 409 | saveRefreshToken: function (token, clientId, expires, user, cb) { 410 | cb(); 411 | } 412 | }, 413 | grants: ['password', 'refresh_token'] 414 | }); 415 | 416 | request(app) 417 | .post('/oauth/token') 418 | .set('Content-Type', 'application/x-www-form-urlencoded') 419 | .send(validBody) 420 | .expect(200) 421 | .end(function (err, res) { 422 | if (err) return done(err); 423 | 424 | res.body.should.have.keys(['access_token', 'token_type', 'expires_in', 425 | 'refresh_token']); 426 | res.body.access_token.should.be.instanceOf(String); 427 | res.body.access_token.should.have.length(40); 428 | res.body.refresh_token.should.be.instanceOf(String); 429 | res.body.refresh_token.should.have.length(40); 430 | res.body.token_type.should.equal('bearer'); 431 | res.body.expires_in.should.equal(3600); 432 | 433 | done(); 434 | }); 435 | 436 | }); 437 | 438 | it('should exclude expires_in if accessTokenLifetime = null', function (done) { 439 | var app = bootstrap({ 440 | model: { 441 | getClient: function (id, secret, callback) { 442 | callback(false, { clientId: 'thom' }); 443 | }, 444 | grantTypeAllowed: function (clientId, grantType, callback) { 445 | callback(false, true); 446 | }, 447 | getUser: function (uname, pword, callback) { 448 | callback(false, { id: 1 }); 449 | }, 450 | saveAccessToken: function (token, clientId, expires, user, cb) { 451 | should.strictEqual(null, expires); 452 | cb(); 453 | }, 454 | saveRefreshToken: function (token, clientId, expires, user, cb) { 455 | should.strictEqual(null, expires); 456 | cb(); 457 | } 458 | }, 459 | grants: ['password', 'refresh_token'], 460 | accessTokenLifetime: null, 461 | refreshTokenLifetime: null 462 | }); 463 | 464 | request(app) 465 | .post('/oauth/token') 466 | .set('Content-Type', 'application/x-www-form-urlencoded') 467 | .send(validBody) 468 | .expect(200) 469 | .end(function (err, res) { 470 | if (err) return done(err); 471 | 472 | res.body.should.have.keys(['access_token', 'refresh_token', 'token_type']); 473 | res.body.access_token.should.be.instanceOf(String); 474 | res.body.access_token.should.have.length(40); 475 | res.body.refresh_token.should.be.instanceOf(String); 476 | res.body.refresh_token.should.have.length(40); 477 | res.body.token_type.should.equal('bearer'); 478 | 479 | done(); 480 | }); 481 | 482 | }); 483 | 484 | it('should continue after success response if continueAfterResponse1 = true', function (done) { 485 | var app = bootstrap({ 486 | model: { 487 | getClient: function (id, secret, callback) { 488 | callback(false, { clientId: 'thom' }); 489 | }, 490 | grantTypeAllowed: function (clientId, grantType, callback) { 491 | callback(false, true); 492 | }, 493 | getUser: function (uname, pword, callback) { 494 | callback(false, { id: 1 }); 495 | }, 496 | saveAccessToken: function (token, clientId, expires, user, cb) { 497 | cb(); 498 | } 499 | }, 500 | grants: ['password'], 501 | continueAfterResponse: true 502 | }); 503 | 504 | var hit = false; 505 | app.all('*', function (req, res, done) { 506 | hit = true; 507 | }); 508 | 509 | request(app) 510 | .post('/oauth/token') 511 | .set('Content-Type', 'application/x-www-form-urlencoded') 512 | .send(validBody) 513 | .expect(200) 514 | .end(function (err, res) { 515 | if (err) return done(err); 516 | hit.should.equal(true); 517 | done(); 518 | }); 519 | }); 520 | 521 | }); 522 | 523 | }); 524 | --------------------------------------------------------------------------------