├── lib ├── middleware │ ├── index.js │ └── bearer.js ├── controller │ ├── authorization │ │ ├── index.js │ │ ├── decision.js │ │ ├── code.js │ │ └── implicit.js │ ├── index.js │ ├── token │ │ ├── index.js │ │ ├── clientCredentials.js │ │ ├── authorizationCode.js │ │ ├── password.js │ │ └── refreshToken.js │ ├── token.js │ └── authorization.js ├── model │ ├── index.js │ ├── user.js │ ├── code.js │ ├── accessToken.js │ ├── refreshToken.js │ └── client.js ├── error │ ├── serverError.js │ ├── accessDenied.js │ ├── invalidGrant.js │ ├── invalidScope.js │ ├── invalidClient.js │ ├── invalidRequest.js │ ├── unauthorizedClient.js │ ├── oauth2.js │ ├── forbidden.js │ ├── unsupportedGrantType.js │ ├── unsupportedResponseType.js │ └── index.js ├── events │ └── index.js ├── index.js └── util │ ├── logger.js │ └── response.js ├── test ├── server │ ├── model │ │ ├── memory │ │ │ ├── index.js │ │ │ └── oauth2 │ │ │ │ ├── index.js │ │ │ │ ├── user.js │ │ │ │ ├── client.js │ │ │ │ ├── code.js │ │ │ │ ├── accessToken.js │ │ │ │ └── refreshToken.js │ │ ├── redis │ │ │ ├── index.js │ │ │ ├── redis.js │ │ │ ├── oauth2 │ │ │ │ ├── index.js │ │ │ │ ├── client.js │ │ │ │ ├── user.js │ │ │ │ ├── code.js │ │ │ │ ├── refreshToken.js │ │ │ │ └── accessToken.js │ │ │ └── data.js │ │ ├── rethinkdb │ │ │ ├── index.js │ │ │ ├── config.js │ │ │ ├── oauth2 │ │ │ │ ├── index.js │ │ │ │ ├── client.js │ │ │ │ ├── user.js │ │ │ │ ├── code.js │ │ │ │ ├── refreshToken.js │ │ │ │ └── accessToken.js │ │ │ ├── connection.js │ │ │ └── data.js │ │ └── data.js │ ├── config.js │ ├── view │ │ ├── authorization.jade │ │ └── login.jade │ ├── oauth20.js │ └── app.js ├── clientCredentials.js ├── password_checkRefreshTokenGrant.js ├── events.js ├── password.js ├── implicit.js ├── refreshToken.js ├── authorizationCode.js └── authorizationCode_checkRefreshTokenGrant.js ├── .gitignore ├── Vagrantfile ├── bootstrap.sh ├── LICENSE ├── package.json └── README.md /lib/middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bearer: require('./bearer.js') 3 | }; -------------------------------------------------------------------------------- /test/server/model/memory/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | oauth2: require('./oauth2') 3 | }; -------------------------------------------------------------------------------- /test/server/model/redis/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | oauth2: require('./oauth2') 3 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | oauth2: require('./oauth2') 3 | }; -------------------------------------------------------------------------------- /test/server/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | host: 'localhost', 4 | port: 60185 5 | } 6 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: '127.0.0.1', 3 | port: 28015, 4 | db: 'oauth' 5 | }; -------------------------------------------------------------------------------- /lib/controller/authorization/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | code: require('./code'), 3 | implicit: require('./implicit') 4 | }; 5 | -------------------------------------------------------------------------------- /lib/controller/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | authorization: require('./authorization.js'), 3 | token: require('./token.js') 4 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NodeJS 2 | node_modules 3 | npm-debug.log 4 | 5 | # OS Files 6 | .DS_Store 7 | 8 | # Vagrant 9 | .vagrant 10 | 11 | # JetBrains IDEs 12 | .idea 13 | -------------------------------------------------------------------------------- /test/server/model/redis/redis.js: -------------------------------------------------------------------------------- 1 | var 2 | redis = require('redis'); 3 | 4 | module.exports = redis.createClient(); 5 | 6 | // No need to wait data load 7 | require('./data.js').initialize(); 8 | -------------------------------------------------------------------------------- /test/server/view/authorization.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Please make your decision 5 | body 6 | div 7 | span 8 | {{client.id}} 9 | form(method='post') 10 | input(type='submit') -------------------------------------------------------------------------------- /lib/controller/token/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | authorizationCode: require('./authorizationCode.js'), 3 | clientCredentials: require('./clientCredentials.js'), 4 | password: require('./password.js'), 5 | refreshToken: require('./refreshToken.js') 6 | }; -------------------------------------------------------------------------------- /test/server/model/memory/oauth2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accessToken: require('./accessToken.js'), 3 | client: require('./client.js'), 4 | code: require('./code.js'), 5 | refreshToken: require('./refreshToken.js'), 6 | user: require('./user.js') 7 | }; -------------------------------------------------------------------------------- /test/server/model/redis/oauth2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accessToken: require('./accessToken.js'), 3 | client: require('./client.js'), 4 | code: require('./code.js'), 5 | refreshToken: require('./refreshToken.js'), 6 | user: require('./user.js') 7 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/oauth2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accessToken: require('./accessToken.js'), 3 | client: require('./client.js'), 4 | code: require('./code.js'), 5 | refreshToken: require('./refreshToken.js'), 6 | user: require('./user.js') 7 | }; -------------------------------------------------------------------------------- /test/server/view/login.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Please authorize yourself 5 | body 6 | div 7 | form(method='post') 8 | input(type='text', name='username') 9 | input(type='password', name='password') 10 | input(type='submit') -------------------------------------------------------------------------------- /lib/model/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accessToken: require('./accessToken.js'), 3 | client: require('./client.js'), 4 | code: require('./code.js'), 5 | refreshToken: require('./refreshToken.js'), 6 | user: require('./user.js') 7 | }; 8 | // @todo: find and remove unnecessary methods in all the models -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | VAGRANTFILE_API_VERSION = "2" 2 | 3 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 4 | config.vm.box = "ubuntu/trusty64" 5 | 6 | config.vm.provision :shell, path: "bootstrap.sh" 7 | 8 | config.vm.network "forwarded_port", guest: 60185, host: 60185 9 | 10 | config.vm.provider "virtualbox" do |vb| 11 | vb.customize ["modifyvm", :id, "--nictype1", "virtio"] 12 | end 13 | end -------------------------------------------------------------------------------- /lib/error/serverError.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var serverError = function (msg) { 6 | serverError.super_.call(this, 'server_error', msg, 500, this.constructor); 7 | }; 8 | util.inherits(serverError, oauth2); 9 | serverError.prototype.name = 'OAuth2ServerError'; 10 | serverError.prototype.logLevel = 'error'; 11 | 12 | module.exports = serverError; -------------------------------------------------------------------------------- /lib/error/accessDenied.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var accessDenied = function (msg) { 6 | accessDenied.super_.call(this, 'access_denied', msg, 403, this.constructor); 7 | }; 8 | util.inherits(accessDenied, oauth2); 9 | accessDenied.prototype.name = 'OAuth2AccessDenied'; 10 | accessDenied.prototype.logLevel = 'info'; 11 | 12 | module.exports = accessDenied; -------------------------------------------------------------------------------- /lib/error/invalidGrant.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var invalidGrant = function (msg) { 6 | invalidGrant.super_.call(this, 'invalid_grant', msg, 400, this.constructor); 7 | }; 8 | util.inherits(invalidGrant, oauth2); 9 | invalidGrant.prototype.name = 'OAuth2InvalidGrant'; 10 | invalidGrant.prototype.logLevel = 'info'; 11 | 12 | module.exports = invalidGrant; -------------------------------------------------------------------------------- /lib/error/invalidScope.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var invalidScope = function (msg) { 6 | invalidScope.super_.call(this, 'invalid_scope', msg, 400, this.constructor); 7 | }; 8 | util.inherits(invalidScope, oauth2); 9 | invalidScope.prototype.name = 'OAuth2InvalidScope'; 10 | invalidScope.prototype.logLevel = 'info'; 11 | 12 | module.exports = invalidScope; -------------------------------------------------------------------------------- /lib/error/invalidClient.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var invalidClient = function (msg) { 6 | invalidClient.super_.call(this, 'invalid_client', msg, 401, this.constructor); 7 | }; 8 | util.inherits(invalidClient, oauth2); 9 | invalidClient.prototype.name = 'OAuth2InvalidClient'; 10 | invalidClient.prototype.logLevel = 'info'; 11 | 12 | module.exports = invalidClient; -------------------------------------------------------------------------------- /lib/error/invalidRequest.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var invalidRequest = function (msg) { 6 | invalidRequest.super_.call(this, 'invalid_request', msg, 400, this.constructor); 7 | }; 8 | util.inherits(invalidRequest, oauth2); 9 | invalidRequest.prototype.name = 'OAuth2InvalidRequest'; 10 | invalidRequest.prototype.logLevel = 'info'; 11 | 12 | module.exports = invalidRequest; -------------------------------------------------------------------------------- /lib/error/unauthorizedClient.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var unauthorizedClient = function (msg) { 6 | unauthorizedClient.super_.call(this, 'unauthorized_client', msg, 400, this.constructor); 7 | }; 8 | util.inherits(unauthorizedClient, oauth2); 9 | unauthorizedClient.prototype.name = 'OAuth2UnauthorizedClient'; 10 | unauthorizedClient.prototype.logLevel = 'info'; 11 | 12 | module.exports = unauthorizedClient; -------------------------------------------------------------------------------- /lib/error/oauth2.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'); 3 | 4 | var oauth2 = function (code, msg, status, constructor) { 5 | Error.call(this); 6 | Error.captureStackTrace(this, constructor || this.constructor); 7 | 8 | this.code = code; 9 | this.message = msg; 10 | this.status = status; 11 | }; 12 | util.inherits(oauth2, Error); 13 | oauth2.prototype.name = 'OAuth2AbstractError'; 14 | oauth2.prototype.logLevel = 'error'; 15 | 16 | module.exports = oauth2; -------------------------------------------------------------------------------- /lib/error/forbidden.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | // @todo: check standards (and other libraries) for error in case of wrong access_token 6 | var forbidden = function (msg) { 7 | forbidden.super_.call(this, 'forbidden', msg, 403, this.constructor); 8 | }; 9 | util.inherits(forbidden, oauth2); 10 | forbidden.prototype.name = 'OAuth2Forbidden'; 11 | forbidden.prototype.logLevel = 'warn'; 12 | 13 | module.exports = forbidden; -------------------------------------------------------------------------------- /lib/error/unsupportedGrantType.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var unsupportedGrantType = function (msg) { 6 | unsupportedGrantType.super_.call(this, 'unsupported_grant_type', msg, 400, this.constructor); 7 | }; 8 | util.inherits(unsupportedGrantType, oauth2); 9 | unsupportedGrantType.prototype.name = 'OAuth2UnsupportedGrantType'; 10 | unsupportedGrantType.prototype.logLevel = 'info'; 11 | 12 | module.exports = unsupportedGrantType; -------------------------------------------------------------------------------- /lib/error/unsupportedResponseType.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | oauth2 = require('./oauth2.js'); 4 | 5 | var unsupportedResponseType = function (msg) { 6 | unsupportedResponseType.super_.call(this, 'unsupported_response_type', msg, 400, this.constructor); 7 | }; 8 | util.inherits(unsupportedResponseType, oauth2); 9 | unsupportedResponseType.prototype.name = 'OAuth2UnsupportedResponseType'; 10 | unsupportedResponseType.prototype.logLevel = 'info'; 11 | 12 | module.exports = unsupportedResponseType; -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | apt-get update 4 | apt-get install -y nodejs npm 5 | ln -s /usr/bin/nodejs /usr/bin/node 6 | 7 | apt-get install -y redis-server 8 | 9 | echo "deb http://download.rethinkdb.com/apt trusty main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list 10 | wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - 11 | sudo apt-get update 12 | sudo apt-get install -y rethinkdb 13 | sudo cp /etc/rethinkdb/default.conf.sample /etc/rethinkdb/instances.d/instance1.conf 14 | sudo /etc/init.d/rethinkdb restart 15 | 16 | cd /vagrant 17 | npm install -------------------------------------------------------------------------------- /test/server/model/rethinkdb/connection.js: -------------------------------------------------------------------------------- 1 | var config = require('./config.js'), 2 | RethinkDb = require('rethinkdb'); 3 | 4 | // Using only one active connection for test purposes 5 | var _connection; 6 | 7 | module.exports.acquire = function(cb) { 8 | if (_connection) return cb(null, _connection); 9 | 10 | RethinkDb.connect(config, function(err, connection) { 11 | if (err) cb(err); 12 | else { 13 | _connection = connection; 14 | cb(null, _connection); 15 | } 16 | }); 17 | }; 18 | 19 | module.exports.close = function(cb) { 20 | if (!_connection) return cb(); 21 | 22 | _connection.close(cb); 23 | }; -------------------------------------------------------------------------------- /lib/error/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accessDenied: require('./accessDenied.js'), 3 | forbidden: require('./forbidden.js'), 4 | invalidClient: require('./invalidClient.js'), 5 | invalidGrant: require('./invalidGrant.js'), 6 | invalidRequest: require('./invalidRequest.js'), 7 | invalidScope: require('./invalidScope.js'), 8 | oauth2: require('./oauth2.js'), 9 | serverError: require('./serverError.js'), 10 | unauthorizedClient: require('./unauthorizedClient.js'), 11 | unsupportedGrantType: require('./unsupportedGrantType.js'), 12 | unsupportedResponseType: require('./unsupportedResponseType.js') 13 | }; -------------------------------------------------------------------------------- /lib/controller/authorization/decision.js: -------------------------------------------------------------------------------- 1 | var error = require('./../../error'); 2 | 3 | /** 4 | * Decision controller 5 | * Used for: "authorization_code" 6 | * Page is used to ask user whether user agree or not to allow client to access his information with current scope 7 | * It should return a POST form with "decision" parameter: 8 | * 0 - if user does not allow client to obtain access 9 | * 1 - if user allows 10 | * For basic example look into ./test/server/oauth20.js 11 | * 12 | * @param req Request object 13 | * @param res Response object 14 | * @param client Client object 15 | * @param scope Scope asked 16 | * @param user User object 17 | */ 18 | module.exports = function(req, res, client, scope, user) { 19 | throw new error.serverError('Decision page is not implemented'); 20 | }; -------------------------------------------------------------------------------- /test/server/model/memory/oauth2/user.js: -------------------------------------------------------------------------------- 1 | var users = require('./../../data.js').users; 2 | 3 | module.exports.getId = function(user) { 4 | return user.id; 5 | }; 6 | 7 | module.exports.fetchById = function(id, cb) { 8 | for (var i in users) { 9 | if (id == users[i].id) return cb(null, users[i]); 10 | }; 11 | cb(); 12 | }; 13 | 14 | module.exports.fetchByUsername = function(username, cb) { 15 | for (var i in users) { 16 | if (username == users[i].username) return cb(null, users[i]); 17 | }; 18 | cb(); 19 | }; 20 | 21 | module.exports.checkPassword = function(user, password, cb) { 22 | (user.password == password) ? cb(null, true) : cb(null, false); 23 | }; 24 | 25 | module.exports.fetchFromRequest = function(req) { 26 | return req.session.user; 27 | }; -------------------------------------------------------------------------------- /test/server/model/memory/oauth2/client.js: -------------------------------------------------------------------------------- 1 | var clients = require('./../../data.js').clients; 2 | 3 | module.exports.getId = function(client) { 4 | return client.id; 5 | }; 6 | 7 | module.exports.getRedirectUri = getRedirectUri; 8 | 9 | module.exports.checkRedirectUri = checkRedirectUri; 10 | 11 | module.exports.fetchById = function(clientId, cb) { 12 | for (var i in clients) { 13 | if (clientId == clients[i].id) return cb(null, clients[i]); 14 | } 15 | cb(); 16 | }; 17 | 18 | module.exports.checkSecret = function(client, secret, cb) { 19 | return cb(null, client.secret == secret); 20 | }; 21 | 22 | function getRedirectUri(client) { 23 | return client.redirectUri; 24 | }; 25 | 26 | function checkRedirectUri(client, redirectUri) { 27 | return (redirectUri.indexOf(getRedirectUri(client)) === 0 && 28 | redirectUri.replace(getRedirectUri(client), '').indexOf('#') === -1); 29 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/oauth2/client.js: -------------------------------------------------------------------------------- 1 | var RethinkDb = require('rethinkdb'), 2 | connection = require('./../connection.js'); 3 | 4 | var TABLE = 'client'; 5 | 6 | module.exports.getId = function(client) { 7 | return client.id; 8 | }; 9 | 10 | module.exports.getRedirectUri = getRedirectUri; 11 | 12 | module.exports.checkRedirectUri = checkRedirectUri; 13 | 14 | module.exports.fetchById = function(clientId, cb) { 15 | connection.acquire(function(err, conn) { 16 | if (err) cb(err); 17 | else RethinkDb.table(TABLE).get(clientId).run(conn, cb); 18 | }); 19 | }; 20 | 21 | module.exports.checkSecret = function(client, secret, cb) { 22 | return cb(null, client.secret == secret); 23 | }; 24 | 25 | function getRedirectUri(client) { 26 | return client.redirectUri; 27 | } 28 | 29 | function checkRedirectUri(client, redirectUri) { 30 | return (redirectUri.indexOf(getRedirectUri(client)) === 0 && 31 | redirectUri.replace(getRedirectUri(client), '').indexOf('#') === -1); 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Tim Shamilov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/server/model/data.js: -------------------------------------------------------------------------------- 1 | // In-memory storage 2 | module.exports = { 3 | users: [ 4 | { 5 | id: 'user1.id', 6 | username: 'user1.username', 7 | password: 'user1.password' 8 | } 9 | ], 10 | clients: [ 11 | { 12 | id: 'client1.id', 13 | name: 'client1.name', 14 | secret: 'client1.secret', 15 | redirectUri: 'http://example.org/oauth2' 16 | }, 17 | { 18 | id: 'client2.id', 19 | name: 'client2.name', 20 | secret: 'client2.Secret', 21 | redirectUri: 'http://example.org/oauth2' 22 | }, 23 | { 24 | id: 'client3.id', 25 | name: 'client3.name', 26 | secret: 'client3.Secret', 27 | redirectUri: 'http://example.org/oauth3' 28 | } 29 | ], 30 | codes: [], 31 | accessTokens: [], 32 | refreshTokens: [] 33 | }; -------------------------------------------------------------------------------- /test/server/model/redis/data.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | util = require('util'), 3 | redis = require('./redis.js'), 4 | data = require('./../data.js'); 5 | 6 | module.exports.initialize = function() { 7 | async.parallel([ 8 | // Insert user 9 | function(cb) { 10 | var model = require('./oauth2/user.js'); 11 | async.eachSeries(data.users, function(user, cb) { 12 | redis.set(util.format(model.KEY.USER, user.id), JSON.stringify(user), function(err) { 13 | if (err) return cb(err); 14 | redis.set(util.format(model.KEY.USER_USERNAME, user.username), user.id, cb) 15 | }) 16 | }, cb); 17 | }, 18 | // Insert client 19 | function(cb) { 20 | var model = require('./oauth2/client.js'); 21 | async.eachSeries(data.clients, function(client, cb) { 22 | redis.set(util.format(model.KEY.CLIENT, client.id), JSON.stringify(client), cb); 23 | }, cb) 24 | } 25 | ], function(err) { 26 | if (err) throw new Error('Unable to fill redis with test data'); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /test/server/model/memory/oauth2/code.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | codes = require('./../../data.js').codes; 3 | 4 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 5 | var code = crypto.randomBytes(32).toString('hex'); 6 | var obj = {code: code, userId: userId, clientId: clientId, scope: scope, ttl: new Date().getTime() + ttl * 1000}; 7 | codes.push(obj); 8 | cb(null, code); 9 | }; 10 | 11 | module.exports.fetchByCode = function(code, cb) { 12 | for (var i in codes) { 13 | if (codes[i].code == code) return cb(null, codes[i]); 14 | } 15 | cb(); 16 | }; 17 | 18 | module.exports.getUserId = function(code) { 19 | return code.userId; 20 | }; 21 | 22 | module.exports.getClientId = function(code) { 23 | return code.clientId; 24 | }; 25 | 26 | module.exports.getScope = function(code) { 27 | return code.scope; 28 | }; 29 | 30 | module.exports.checkTtl = function(code) { 31 | return (code.ttl > new Date().getTime()); 32 | }; 33 | 34 | module.exports.removeByCode = function(code, cb) { 35 | for (var i in codes) { 36 | if (codes[i].code == code) { 37 | codes.splice(i, 1); 38 | break; 39 | } 40 | } 41 | cb(); 42 | }; -------------------------------------------------------------------------------- /lib/events/index.js: -------------------------------------------------------------------------------- 1 | var events = require('events'), 2 | util = require('util'); 3 | 4 | function _events(){ 5 | 6 | events.call(this); 7 | 8 | this.log = function emit_log(level, message){ 9 | this.emit('log',level, message); 10 | }; 11 | 12 | this.uncaught_exception = function emit_uncaught_exception(req, err){ 13 | this.emit('OAuth2UncaughtException', req, err); 14 | }; 15 | 16 | this.caught_exception = function emit_caught_exception(req, err){ 17 | this.emit(err.name, req, err); 18 | }; 19 | 20 | this.authorization_code_granted = function emit_authorization_code_granted(req, code){ 21 | this.emit('authorization_code_granted', req, code); 22 | }; 23 | 24 | this.authorization_implicit_granted = function emit_authorization_implicit_granted(req, token){ 25 | this.emit('authorization_implicit_granted', req, token); 26 | }; 27 | 28 | this.token_granted = function emit_token_granted(event, req, token){ 29 | this.emit(event, req, token); 30 | }; 31 | 32 | this.access_token_fetched = function emit_access_token_fetched(req,token){ 33 | this.emit('access_token_fetched', req, token); 34 | }; 35 | } 36 | 37 | util.inherits(_events, events); 38 | 39 | module.exports = new _events(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth20-provider", 3 | "version": "0.6.0", 4 | "description": "OAuth 2.0 provider toolkit for nodeJS", 5 | "keywords": [ 6 | "oauth", 7 | "oauth2", 8 | "provider", 9 | "server", 10 | "connect", 11 | "express", 12 | "middleware", 13 | "http", 14 | "api", 15 | "rest" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/t1msh/node-oauth20-provider.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/t1msh/node-oauth20-provider/issues" 23 | }, 24 | "author": { 25 | "name": "Tim", 26 | "email": "tim.shamilov@gmail.com" 27 | }, 28 | "licenses": [ 29 | { 30 | "type": "MIT", 31 | "url": "http://www.opensource.org/licenses/MIT" 32 | } 33 | ], 34 | "main": "./lib", 35 | "dependencies": { 36 | "async": "*" 37 | }, 38 | "devDependencies": { 39 | "body-parser": "*", 40 | "cookie-parser": "*", 41 | "express": "*", 42 | "express-session": "*", 43 | "jade": "*", 44 | "mocha": "*", 45 | "moment": "^2.10.3", 46 | "redis": "*", 47 | "supertest": "*" 48 | }, 49 | "scripts": { 50 | "test": "node_modules/.bin/mocha --reporter spec" 51 | }, 52 | "engines": { 53 | "node": ">= 0.10.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/server/model/redis/oauth2/client.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | redis = require('./../redis.js'); 4 | 5 | var KEY = { 6 | CLIENT: 'client:%s' 7 | }; 8 | 9 | module.exports.KEY = KEY; 10 | 11 | module.exports.getId = function(client) { 12 | return client.id; 13 | }; 14 | 15 | module.exports.getRedirectUri = getRedirectUri; 16 | 17 | module.exports.checkRedirectUri = checkRedirectUri; 18 | 19 | module.exports.fetchById = function(clientId, cb) { 20 | redis.get(util.format(KEY.CLIENT, clientId), function(err, stringified) { 21 | if (err) cb(err); 22 | else if (!stringified) cb(); 23 | else { 24 | try { 25 | var obj = JSON.parse(stringified); 26 | cb(null, obj); 27 | } catch (e) { 28 | cb(); 29 | } 30 | } 31 | }); 32 | }; 33 | 34 | // Add some hashing algorithm for security 35 | module.exports.checkSecret = function(client, secret, cb) { 36 | return cb(null, client.secret == secret); 37 | }; 38 | 39 | function getRedirectUri(client) { 40 | return client.redirectUri; 41 | } 42 | 43 | function checkRedirectUri(client, redirectUri) { 44 | return (redirectUri.indexOf(getRedirectUri(client)) === 0 && 45 | redirectUri.replace(getRedirectUri(client), '').indexOf('#') === -1); 46 | } -------------------------------------------------------------------------------- /test/server/model/rethinkdb/oauth2/user.js: -------------------------------------------------------------------------------- 1 | var RethinkDb = require('rethinkdb'), 2 | connection = require('./../connection.js'); 3 | 4 | var TABLE = 'user'; 5 | 6 | module.exports.getId = function(user) { 7 | return user['id']; 8 | }; 9 | 10 | module.exports.fetchById = function(id, cb) { 11 | connection.acquire(function(err, conn) { 12 | if (err) cb(err); 13 | else RethinkDb.table(TABLE).get(id).run(conn, cb); 14 | }); 15 | }; 16 | 17 | module.exports.fetchByUsername = function(username, cb) { 18 | connection.acquire(function(err, conn) { 19 | if (err) cb(err); 20 | else { 21 | RethinkDb.table(TABLE).filter({ username: username }).limit(1).run(conn, function(err, cursor) { 22 | if (err) cb(err); 23 | else { 24 | cursor.toArray(function(err, users) { 25 | if (err) cb(err); 26 | else cb(err, users && users.length ? users[0] : null); 27 | }); 28 | } 29 | }); 30 | } 31 | }); 32 | }; 33 | 34 | module.exports.checkPassword = function(user, password, cb) { 35 | (user.password == password) ? cb(null, true) : cb(null, false); 36 | }; 37 | 38 | module.exports.fetchFromRequest = function(req) { 39 | return req.session.user; 40 | }; -------------------------------------------------------------------------------- /test/clientCredentials.js: -------------------------------------------------------------------------------- 1 | var 2 | request = require('supertest'), 3 | data = require('./server/model/data.js'), 4 | app = require('./server/app.js'); 5 | 6 | describe('Client Credentials Grant Type ',function() { 7 | 8 | var 9 | accessToken; 10 | 11 | it('POST /token with grant_type="client_credentials" expect token', function(done) { 12 | request(app) 13 | .post('/token') 14 | .set('Authorization', 'Basic ' + new Buffer(data.clients[0].id + ':' + data.clients[0].secret, 'ascii').toString('base64')) 15 | .send({grant_type: 'client_credentials'}) 16 | .expect(200, /access_token/) 17 | .end(function(err, res) { 18 | if (err) return done(err); 19 | accessToken = res.body.access_token; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('POST /secure expect forbidden', function(done) { 25 | request(app) 26 | .get('/secure') 27 | .set('Authorization', 'Bearer ' + accessToken) 28 | .expect(403, done); 29 | }); 30 | 31 | it('POST /client expect authorized', function(done) { 32 | request(app) 33 | .get('/client') 34 | .set('Authorization', 'Bearer ' + accessToken) 35 | .expect(200, done); 36 | }); 37 | 38 | }); -------------------------------------------------------------------------------- /test/server/model/redis/oauth2/user.js: -------------------------------------------------------------------------------- 1 | var 2 | util = require('util'), 3 | redis = require('./../redis.js'); 4 | 5 | var KEY = { 6 | USER : 'user:id:%s', 7 | USER_USERNAME: 'user:username:%s' 8 | }; 9 | 10 | module.exports.KEY = KEY; 11 | 12 | module.exports.getId = function(user) { 13 | return user.id; 14 | }; 15 | 16 | var fetchById = function(id, cb) { 17 | redis.get(util.format(KEY.USER, id), function(err, stringified) { 18 | if (err) cb(err); 19 | else if (!stringified) cb(); 20 | else { 21 | try { 22 | var obj = JSON.parse(stringified); 23 | cb(null, obj); 24 | } catch (e) { 25 | cb(); 26 | } 27 | } 28 | }); 29 | }; 30 | 31 | module.exports.fetchById = fetchById; 32 | 33 | module.exports.fetchByUsername = function(username, cb) { 34 | redis.get(util.format(KEY.USER_USERNAME, username), function(err, userId) { 35 | if (err) cb(err); 36 | else if (!userId) cb(); 37 | else { 38 | fetchById(userId, cb); 39 | } 40 | }); 41 | }; 42 | 43 | module.exports.checkPassword = function(user, password, cb) { 44 | (user.password == password) ? cb(null, true) : cb(null, false); 45 | }; 46 | 47 | module.exports.fetchFromRequest = function(req) { 48 | return req.session.user; 49 | }; -------------------------------------------------------------------------------- /test/server/model/memory/oauth2/accessToken.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | accessTokens = require('./../../data.js').accessTokens, 3 | moment = require('moment'); 4 | 5 | module.exports.getToken = function(accessToken) { 6 | return accessToken.token; 7 | }; 8 | 9 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 10 | var token = crypto.randomBytes(64).toString('hex'); 11 | var obj = {token: token, userId: userId, clientId: clientId, scope: scope, ttl: new Date().getTime() + ttl * 1000}; 12 | accessTokens.push(obj); 13 | cb(null, token); 14 | }; 15 | 16 | module.exports.fetchByToken = function(token, cb) { 17 | for (var i in accessTokens) { 18 | if (accessTokens[i].token == token) return cb(null, accessTokens[i]); 19 | } 20 | cb(); 21 | }; 22 | 23 | module.exports.checkTTL = function(accessToken) { 24 | return (accessToken.ttl > new Date().getTime()); 25 | }; 26 | 27 | module.exports.getTTL = function(accessToken, cb) { 28 | var ttl = moment(accessToken.ttl).diff(new Date(),'seconds'); 29 | return cb(null, ttl>0?ttl:0); 30 | }; 31 | 32 | module.exports.fetchByUserIdClientId = function(userId, clientId, cb) { 33 | for (var i in accessTokens) { 34 | if (accessTokens[i].userId == userId && accessTokens[i].clientId == clientId) return cb(null, accessTokens[i]); 35 | }; 36 | cb(); 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var 2 | model = require('./model/'), 3 | controller = require('./controller'), 4 | middleware = require('./middleware'), 5 | decision = require('./controller/authorization/decision'), 6 | logger = require('./util/logger.js'), 7 | emitter = require('./events'); 8 | 9 | var oauth2 = function(options) { 10 | var _self = this; 11 | 12 | options = options || {}; 13 | 14 | options.log = options.log || { 15 | level: 0, 16 | color: true, 17 | emit_event: false 18 | }; 19 | 20 | // options.flows = options.flows || [ 21 | // 'authorization_code', 22 | // 'implicit', 23 | // 'password', 24 | // 'client_credentials' 25 | // ]; 26 | 27 | this.options = options; 28 | 29 | // Initialize objects (available for redefinition) 30 | this.logger = new logger(this.options.log); 31 | this.model = model; 32 | this.decision = decision; 33 | 34 | this.logger.info('OAuth2 library initialized'); 35 | 36 | // Injection method 37 | this.inject = function() { 38 | return function(req, res, next) { 39 | _self.logger.debug('Injecting oauth2 into request'); 40 | req.oauth2 = _self; 41 | next(); 42 | } 43 | }; 44 | }; 45 | 46 | oauth2.prototype.controller = controller; 47 | oauth2.prototype.middleware = middleware; 48 | oauth2.prototype.events = emitter; 49 | 50 | module.exports = oauth2; -------------------------------------------------------------------------------- /test/server/model/memory/oauth2/refreshToken.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | refreshTokens = require('./../../data.js').refreshTokens; 3 | 4 | module.exports.getUserId = function(refreshToken) { 5 | return refreshToken.userId; 6 | }; 7 | 8 | module.exports.getClientId = function(refreshToken) { 9 | return refreshToken.clientId; 10 | }; 11 | 12 | module.exports.getScope = function(refreshToken) { 13 | return refreshToken.scope; 14 | }; 15 | 16 | module.exports.fetchByToken = function(token, cb) { 17 | for (var i in refreshTokens) { 18 | if (refreshTokens[i].token == token) return cb(null, refreshTokens[i]); 19 | } 20 | cb(null, null); 21 | }; 22 | 23 | module.exports.removeByUserIdClientId = function(userId, clientId, cb) { 24 | for (var i in refreshTokens) { 25 | if (refreshTokens[i].userId == userId && refreshTokens[i].clientId == clientId) 26 | refreshTokens.splice(i, 1); 27 | } 28 | cb(); 29 | }; 30 | 31 | module.exports.removeByRefreshToken = function(refreshToken, cb) { 32 | for (var i in refreshTokens) { 33 | if (refreshTokens[i].token == refreshToken) 34 | refreshTokens.splice(i, 1); 35 | } 36 | cb(); 37 | }; 38 | 39 | module.exports.create = function(userId, clientId, scope, cb) { 40 | var token = crypto.randomBytes(64).toString('hex'); 41 | var obj = {token: token, userId: userId, clientId: clientId, scope: scope}; 42 | refreshTokens.push(obj); 43 | cb(null, token); 44 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/oauth2/code.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | RethinkDb = require('rethinkdb'), 3 | connection = require('./../connection.js'); 4 | 5 | var TABLE = 'authorization_code'; 6 | 7 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 8 | var code = crypto.randomBytes(32).toString('hex'); 9 | var obj = {code: code, userId: userId, clientId: clientId, scope: scope, ttl: new Date().getTime() + ttl * 1000}; 10 | 11 | connection.acquire(function(err, conn) { 12 | if (err) cb(err); 13 | else RethinkDb.table(TABLE).insert(obj, {}).run(conn, function(err) { 14 | cb(err, code); 15 | }); 16 | }); 17 | }; 18 | 19 | module.exports.fetchByCode = function(code, cb) { 20 | connection.acquire(function(err, conn) { 21 | if (err) cb(err); 22 | else { 23 | RethinkDb.table(TABLE).filter({ code: code }).limit(1).run(conn, function(err, cursor) { 24 | if (err) cb(err); 25 | else cursor.next(cb); 26 | }); 27 | } 28 | }); 29 | }; 30 | 31 | module.exports.getUserId = function(code) { 32 | return code.userId; 33 | }; 34 | 35 | module.exports.getClientId = function(code) { 36 | return code.clientId; 37 | }; 38 | 39 | module.exports.getScope = function(code) { 40 | return code.scope; 41 | }; 42 | 43 | module.exports.checkTtl = function(code) { 44 | return (code.ttl > new Date().getTime()); 45 | }; 46 | 47 | module.exports.removeByCode = function(code, cb) { 48 | connection.acquire(function(err, conn) { 49 | if (err) cb(err); 50 | else RethinkDb.table(TABLE).filter({ code: code }).delete().run(conn, cb); 51 | }); 52 | }; -------------------------------------------------------------------------------- /test/server/model/redis/oauth2/code.js: -------------------------------------------------------------------------------- 1 | var 2 | crypto = require('crypto'), 3 | util = require('util'), 4 | redis = require('./../redis.js'); 5 | 6 | var KEY = { 7 | CODE: 'code:%s' 8 | }; 9 | 10 | module.exports.KEY = KEY; 11 | 12 | module.exports.getUserId = function(code) { 13 | return code.userId; 14 | }; 15 | 16 | module.exports.getClientId = function(code) { 17 | return code.clientId; 18 | }; 19 | 20 | module.exports.getScope = function(code) { 21 | return code.scope; 22 | }; 23 | 24 | module.exports.checkTtl = function(code) { 25 | // No need to check in redis storage because of key expiry mechanism 26 | return true; 27 | }; 28 | 29 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 30 | var code = crypto.randomBytes(32).toString('hex'); 31 | var ttl = new Date().getTime() + ttl * 1000; 32 | var obj = {code: code, userId: userId, clientId: clientId, scope: scope}; 33 | redis.setex(util.format(KEY.CODE, code), ttl, JSON.stringify(obj), function(err) { 34 | if (err) cb(err); 35 | else cb(null, code); 36 | }); 37 | }; 38 | 39 | module.exports.fetchByCode = function(code, cb) { 40 | redis.get(util.format(KEY.CODE, code), function(err, stringified) { 41 | if (err) cb(err); 42 | else if (!stringified) cb(); 43 | else { 44 | try { 45 | var obj = JSON.parse(stringified); 46 | cb(null, obj); 47 | } catch (e) { 48 | cb(); 49 | } 50 | } 51 | }); 52 | }; 53 | 54 | module.exports.removeByCode = function(code, cb) { 55 | redis.del(util.format(KEY.CODE, code), function(err) { 56 | cb(err); 57 | }) 58 | }; -------------------------------------------------------------------------------- /test/server/model/redis/oauth2/refreshToken.js: -------------------------------------------------------------------------------- 1 | var 2 | crypto = require('crypto'), 3 | util = require('util'), 4 | redis = require('./../redis.js'); 5 | 6 | var KEY = { 7 | TOKEN: 'refreshToken:%s' 8 | }; 9 | 10 | module.exports.KEY = KEY; 11 | 12 | module.exports.getUserId = function(refreshToken) { 13 | return refreshToken.userId; 14 | }; 15 | 16 | module.exports.getClientId = function(refreshToken) { 17 | return refreshToken.clientId; 18 | }; 19 | 20 | module.exports.getScope = function(refreshToken) { 21 | return refreshToken.scope; 22 | }; 23 | 24 | module.exports.create = function(userId, clientId, scope, cb) { 25 | var token = crypto.randomBytes(64).toString('hex'); 26 | var obj = {token: token, userId: userId, clientId: clientId, scope: scope}; 27 | redis.set(util.format(KEY.TOKEN, token), JSON.stringify(obj), function(err) { 28 | if (err) cb(err); 29 | else cb(null, token); 30 | }); 31 | }; 32 | 33 | module.exports.fetchByToken = function(token, cb) { 34 | redis.get(util.format(KEY.TOKEN, token), function(err, stringified) { 35 | if (err) cb(err); 36 | else if (!stringified) cb(); 37 | else { 38 | try { 39 | var obj = JSON.parse(stringified); 40 | cb(null, obj); 41 | } catch (e) { 42 | cb(); 43 | } 44 | } 45 | }); 46 | }; 47 | 48 | // @todo: remove old refreshTokens 49 | module.exports.removeByUserIdClientId = function(userId, clientId, cb) { 50 | cb(); 51 | }; 52 | 53 | module.exports.removeByRefreshToken = function(refreshToken, cb) { 54 | redis.del(util.format(KEY.TOKEN, refreshToken), function(err) { 55 | cb(err); 56 | }); 57 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/oauth2/refreshToken.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | RethinkDb = require('rethinkdb'), 3 | connection = require('./../connection.js'); 4 | 5 | var TABLE = 'refresh_token'; 6 | 7 | module.exports.getUserId = function(refreshToken) { 8 | return refreshToken.userId; 9 | }; 10 | 11 | module.exports.getClientId = function(refreshToken) { 12 | return refreshToken.clientId; 13 | }; 14 | 15 | module.exports.getScope = function(refreshToken) { 16 | return refreshToken.scope; 17 | }; 18 | 19 | module.exports.fetchByToken = function(token, cb) { 20 | connection.acquire(function(err, conn) { 21 | if (err) cb(err); 22 | else { 23 | RethinkDb.table(TABLE).filter({token: token}).run(conn, function(err, cursor) { 24 | if (err) cb(err); 25 | else cursor.next(cb); 26 | }); 27 | } 28 | }); 29 | }; 30 | 31 | module.exports.removeByUserIdClientId = function(userId, clientId, cb) { 32 | connection.acquire(function(err, conn) { 33 | if (err) cb(err); 34 | else { 35 | RethinkDb.table(TABLE).filter({ 36 | userId: userId, 37 | clientId: clientId 38 | }).delete().run(conn, cb); 39 | } 40 | }); 41 | }; 42 | 43 | module.exports.create = function(userId, clientId, scope, cb) { 44 | var token = crypto.randomBytes(64).toString('hex'); 45 | var obj = { token: token, userId: userId, clientId: clientId, scope: scope }; 46 | 47 | connection.acquire(function(err, conn) { 48 | if (err) cb(err); 49 | else RethinkDb.table(TABLE).insert(obj, {}).run(conn, function(err) { 50 | cb(err, token); 51 | }); 52 | }); 53 | }; -------------------------------------------------------------------------------- /test/password_checkRefreshTokenGrant.js: -------------------------------------------------------------------------------- 1 | var 2 | request = require('supertest'), 3 | data = require('./server/model/data.js'), 4 | app = require('./server/app.js'); 5 | 6 | describe('Password Grant Type without client\'s refresh token grant type',function() { 7 | 8 | before(function() { 9 | app.get('oauth2').model.client.checkGrantType = function(client, grant){ 10 | return grant != 'refresh_token'; 11 | }; 12 | }); 13 | 14 | after(function(){ 15 | app.get('oauth2').model.client.checkGrantType = function(client, grant){ 16 | return []; 17 | }; 18 | }); 19 | 20 | var 21 | accessToken; 22 | 23 | it('POST /token with grant_type="password" expect token', function(done) { 24 | request(app) 25 | .post('/token') 26 | .set('Authorization', 'Basic ' + new Buffer(data.clients[0].id + ':' + data.clients[0].secret, 'ascii').toString('base64')) 27 | .send({grant_type: 'password', username: data.users[0].username, password: data.users[0].password}) 28 | .expect(200) 29 | .expect(function check_no_refresh_token(res){ 30 | if(res.body.refresh_token){ 31 | throw new Error('refresh_token received') 32 | } 33 | }) 34 | .end(function(err, res) { 35 | if (err) return done(err); 36 | accessToken = res.body.access_token; 37 | done(); 38 | }); 39 | }); 40 | 41 | it('POST /secure expect authorized', function(done) { 42 | request(app) 43 | .get('/secure') 44 | .set('Authorization', 'Bearer ' + accessToken) 45 | .expect(200, new RegExp(data.users[0].id, 'i'), done); 46 | }); 47 | 48 | }); -------------------------------------------------------------------------------- /lib/controller/token/clientCredentials.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | response = require('./../../util/response.js'), 4 | error = require('./../../error'); 5 | 6 | module.exports = function(oauth2, client, scope, pCb) { 7 | 8 | // Define variables 9 | var scope, 10 | accessTokenValue; 11 | var responseObj = { 12 | token_type: "bearer" 13 | }; 14 | 15 | async.waterfall([ 16 | // Parse and check scope against supported and client available scopes 17 | function(cb) { 18 | scope = oauth2.model.client.transformScope(scope); 19 | scope = oauth2.model.client.checkScope(client, scope); 20 | if (!scope) 21 | cb(new error.invalidScope('Invalid scope for the client')); 22 | else { 23 | oauth2.logger.debug('Scope check passed: ', scope); 24 | cb(); 25 | } 26 | }, 27 | // Generate new accessToken and save it 28 | function(cb) { 29 | oauth2.model.accessToken.create(null, oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl, function(err, data) { 30 | if (err) 31 | cb(new error.serverError('Failed to call accessToken::save method')); 32 | else { 33 | responseObj.access_token = data; 34 | responseObj.expires_in = oauth2.model.accessToken.ttl; 35 | oauth2.logger.debug('Access token saved: ', accessTokenValue); 36 | cb(); 37 | } 38 | }); 39 | } 40 | ], 41 | function(err) { 42 | if (err) pCb(err); 43 | else { 44 | pCb(null, { event: 'token_granted_from_client_credentials', data:responseObj}); 45 | } 46 | }); 47 | }; -------------------------------------------------------------------------------- /lib/util/logger.js: -------------------------------------------------------------------------------- 1 | var emitter = require('../events'); 2 | 3 | /** 4 | * Used log levels, from low priority to high 5 | */ 6 | var levels = [ 7 | 'debug', 8 | 'info', 9 | 'warn', 10 | 'error' 11 | ]; 12 | 13 | /** 14 | * Colors for log levels. 15 | */ 16 | var colors = [ 17 | 90, 18 | 36, 19 | 33, 20 | 31 21 | ]; 22 | 23 | /** 24 | * Pads the nice output to the longest log level. 25 | */ 26 | function pad (str) { 27 | var max = 0; 28 | 29 | for (var i = 0, l = levels.length; i < l; i++) 30 | max = Math.max(max, levels[i].length); 31 | 32 | if (str.length < max) 33 | return str + new Array(max - str.length + 1).join(' '); 34 | 35 | return str; 36 | }; 37 | 38 | /** 39 | * Console logging class 40 | * 41 | * @param options 42 | * @constructor 43 | */ 44 | var Logger = function(options) { 45 | // Force options or die 46 | this.colors = false !== options.colors; 47 | this.level = options.level || 0; 48 | this.emit_event = options.emit_event || false; 49 | }; 50 | 51 | /** 52 | * Log method 53 | * 54 | * @api public 55 | */ 56 | Logger.prototype.log = function (type) { 57 | var typeLevel = levels.indexOf(type); 58 | 59 | if (typeLevel < this.level) return; 60 | 61 | var args = [this.colors ? '\033[' + colors[typeLevel] + 'm' + pad(type) + ':\033[39m' : type + ':'] 62 | .concat(Array.prototype.slice.call(arguments, 1)); 63 | 64 | if(this.emit_event){ 65 | return emitter.log.apply(emitter, args); 66 | } 67 | 68 | console.log.apply(console,args); 69 | }; 70 | 71 | /** 72 | * Generate methods for each level 73 | */ 74 | levels.forEach(function (name) { 75 | Logger.prototype[name] = function () { 76 | this.log.apply(this, [name].concat(Array.prototype.slice.call(arguments))); 77 | }; 78 | }); 79 | 80 | 81 | module.exports = Logger; -------------------------------------------------------------------------------- /lib/controller/authorization/code.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | error = require('./../../error'), 4 | response = require('./../../util/response.js'), 5 | emitter = require('./../../events'); 6 | 7 | // @todo: move decision var to config 8 | // @todo: add state 9 | 10 | module.exports = function(req, res, client, scope, user, redirectUri) { 11 | 12 | var 13 | codeValue; 14 | 15 | async.waterfall([ 16 | // Check user decision 17 | function(cb) { 18 | if (!req.body || typeof(req.body['decision']) == 'undefined') 19 | cb(new error.invalidRequest('No decision parameter passed')); 20 | else if (req.body['decision'] == 0) 21 | cb(new error.accessDenied('User denied the access to the resource')); 22 | else { 23 | req.oauth2.logger.debug('Decision check passed'); 24 | cb(); 25 | } 26 | }, 27 | // Issue new code 28 | function(cb) { 29 | req.oauth2.model.code.create(req.oauth2.model.user.getId(user), req.oauth2.model.client.getId(client), scope, req.oauth2.model.code.ttl, function(err, data) { 30 | if (err) 31 | cb(new error.serverError('Failed to call code::save method')); 32 | else { 33 | codeValue = data; 34 | req.oauth2.logger.debug('Access token saved: ', codeValue); 35 | cb(); 36 | } 37 | }); 38 | } 39 | ], 40 | function(err) { 41 | if (err) response.error(req, res, err, redirectUri); 42 | else { 43 | var responseObj = {code: codeValue}; 44 | emitter.authorization_code_granted(req, responseObj); 45 | response.data(req, res, responseObj, redirectUri); 46 | } 47 | }); 48 | }; -------------------------------------------------------------------------------- /test/events.js: -------------------------------------------------------------------------------- 1 | var 2 | query = require('querystring'), 3 | request = require('supertest'), 4 | data = require('./server/model/data.js'), 5 | app = require('./server/app.js'); 6 | 7 | describe('Emit log event',function() { 8 | 9 | before(function() { 10 | app.get('oauth2').logger.emit_event = true; 11 | app.get('oauth2').logger.level = 0; 12 | }); 13 | 14 | after(function(){ 15 | app.get('oauth2').logger.emit_event = false; 16 | app.get('oauth2').logger.level = 4; 17 | }); 18 | 19 | it('Check log event on POST /token with grant_type="client_credentials" expect token', function(done) { 20 | var listener = function(){ 21 | app.get('oauth2').events.removeListener('log', listener); 22 | done(); 23 | }; 24 | app.get('oauth2').events.on('log', listener); 25 | request(app) 26 | .post('/token') 27 | .set('Authorization', 'Basic ' + new Buffer(data.clients[0].id + ':' + data.clients[0].secret, 'ascii').toString('base64')) 28 | .send({grant_type: 'client_credentials'}) 29 | .expect(200, /access_token/) 30 | .end(function(err, res) { 31 | }); 32 | }); 33 | 34 | it('Check a caught exception on POST /token with grant_type="client_credentials" expect token', function(done) { 35 | var listener = function(){ 36 | app.get('oauth2').events.removeListener('OAuth2InvalidClient', listener); 37 | done(); 38 | }; 39 | app.get('oauth2').events.on('OAuth2InvalidClient', listener); 40 | request(app) 41 | .post('/token') 42 | .set('Authorization', 'Basic ' + new Buffer(data.clients[0].id + ':' + 'bad password', 'ascii').toString('base64')) 43 | .send({grant_type: 'client_credentials'}) 44 | .expect(200, /access_token/) 45 | .end(function(err, res) { 46 | }); 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /test/server/model/redis/oauth2/accessToken.js: -------------------------------------------------------------------------------- 1 | var 2 | crypto = require('crypto'), 3 | util = require('util'), 4 | redis = require('./../redis.js'); 5 | 6 | // SOME KEY CONSTANTS 7 | var KEY = { 8 | ACCESS_TOKEN: 'accessToken:%s', 9 | USER_CLIENT_TOKEN: 'userId:%s:clientId:%s' 10 | }; 11 | 12 | module.exports.KEY = KEY; 13 | 14 | module.exports.getToken = function(accessToken) { 15 | return accessToken.token; 16 | }; 17 | 18 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 19 | var token = crypto.randomBytes(64).toString('hex'); 20 | var obj = {token: token, userId: userId, clientId: clientId, scope: scope}; 21 | redis.setex(util.format(KEY.ACCESS_TOKEN, token), ttl, JSON.stringify(obj), function(err, data) { 22 | if (err) cb(err); 23 | else cb(null, token); 24 | }); 25 | redis.setex(util.format(KEY.USER_CLIENT_TOKEN, userId, clientId), ttl, token, function() {}); 26 | }; 27 | 28 | var fetchByToken = function(token, cb) { 29 | redis.get(util.format(KEY.ACCESS_TOKEN, token), function(err, stringified) { 30 | if (err) cb(err); 31 | else if (!stringified) cb(); 32 | else { 33 | try { 34 | var obj = JSON.parse(stringified); 35 | cb(null, obj); 36 | } catch (e) { 37 | cb(); 38 | } 39 | } 40 | }); 41 | }; 42 | 43 | module.exports.fetchByToken = fetchByToken; 44 | 45 | // No need to check expiry due to Redis TTL 46 | module.exports.checkTTL = function(accessToken) { 47 | return true; 48 | }; 49 | 50 | module.exports.getTTL = function(token, cb) { 51 | redis.ttl(util.format(KEY.ACCESS_TOKEN, token), cb); 52 | }; 53 | 54 | module.exports.fetchByUserIdClientId = function(userId, clientId, cb) { 55 | redis.get(util.format(KEY.USER_CLIENT_TOKEN, userId, clientId), function(err, token) { 56 | if (err) cb(err); 57 | else fetchByToken(token, cb); 58 | }); 59 | }; -------------------------------------------------------------------------------- /lib/util/response.js: -------------------------------------------------------------------------------- 1 | var 2 | query = require('querystring'), 3 | error = require('../error/'), 4 | emitter = require('./../events'); 5 | 6 | function data(req, res, code, data) { 7 | res.statusCode = code; 8 | res.header('Cache-Control', 'no-store'); 9 | res.header('Pragma','no-cache'); 10 | res.send(data); 11 | req.oauth2.logger.debug('Response: ', data); 12 | } 13 | 14 | function redirect(req, res, redirectUri) { 15 | res.statusCode = 302; 16 | res.header('Location', redirectUri); 17 | res.end(); 18 | req.oauth2.logger.debug('Redirect to: ', redirectUri); 19 | } 20 | 21 | module.exports.error = function(req, res, err, redirectUri) { 22 | // Transform unknown error 23 | if (!(err instanceof error.oauth2)) { 24 | req.oauth2.logger.error(err.stack); 25 | emitter.uncaught_exception(req, err); 26 | err = new error.serverError('Uncaught exception'); 27 | } 28 | else { 29 | emitter.caught_exception(req, err); 30 | req.oauth2.logger[err.logLevel]('Exception caught', err.stack); 31 | } 32 | 33 | if (redirectUri) { 34 | var obj = { 35 | error: err.code, 36 | error_description: err.message 37 | }; 38 | if (req.query.state) obj.state = req.query.state; 39 | redirectUri += '?' + query.stringify(obj); 40 | redirect(req, res, redirectUri); 41 | } 42 | else 43 | data(req, res, err.status, {error: err.code, error_description: err.message}); 44 | }; 45 | 46 | module.exports.data = function(req, res, obj, redirectUri, anchor) { 47 | if (redirectUri) { 48 | if (anchor) 49 | redirectUri += '#'; 50 | else 51 | redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&'); 52 | if (req.query.state) obj.state = req.query.state; 53 | redirectUri += query.stringify(obj); 54 | redirect(req, res, redirectUri); 55 | } 56 | else 57 | data(req, res, 200, obj); 58 | }; 59 | -------------------------------------------------------------------------------- /lib/model/user.js: -------------------------------------------------------------------------------- 1 | var 2 | error = require('./../error'); 3 | 4 | /** 5 | * User schema is defined by server side logic 6 | */ 7 | 8 | /** 9 | * Gets primary key of the user 10 | * 11 | * @param user {Object} User object 12 | */ 13 | module.exports.getId = function(user) { 14 | throw new error.serverError('User model method "getId" is not implemented'); 15 | }; 16 | 17 | /** 18 | * Fetches user object by primary key 19 | * Should be implemented with server logic 20 | * 21 | * @param userId {String} Unique identifier 22 | * @param cb {Function} Function callback ->(error, object) 23 | */ 24 | module.exports.fetchById = function(userId, cb) { 25 | throw new error.serverError('User model method "fetchById" is not implemented'); 26 | }; 27 | 28 | /** 29 | * Fetches user object by primary key 30 | * Should be implemented with server logic 31 | * 32 | * @param username {String} Unique username/login 33 | * @param cb {Function} Function callback ->(error, object) 34 | */ 35 | module.exports.fetchByUsername = function(username, cb) { 36 | throw new error.serverError('User model method "fetchByUsername" is not implemented'); 37 | }; 38 | 39 | /** 40 | * Checks password for the user 41 | * Function arguments MAY be different 42 | * 43 | * @param user {Object} User object 44 | * @param password {String} Password to be checked 45 | * @param cb {Function} Function callback -> (error, boolean) If input is correct 46 | */ 47 | module.exports.checkPassword = function(user, password, cb) { 48 | /** 49 | * In case of sync check function use: 50 | * (user.password == superHashFunction(password)) ? cb(null, true) : cb(null, false); 51 | */ 52 | throw new error.serverError('User model method "checkPassword" is not implemented'); 53 | }; 54 | 55 | /** 56 | * Fetch user object from session (fetch logged user only) 57 | * 58 | * @param req 59 | */ 60 | module.exports.fetchFromRequest = function(req) { 61 | throw new error.serverError('User model method "fetchFromRequest" is not implemented'); 62 | }; -------------------------------------------------------------------------------- /test/password.js: -------------------------------------------------------------------------------- 1 | var 2 | request = require('supertest'), 3 | data = require('./server/model/data.js'), 4 | app = require('./server/app.js'); 5 | 6 | describe('Password Grant Type ',function() { 7 | 8 | var 9 | refreshToken, 10 | accessToken; 11 | 12 | it('POST /token with grant_type="password" expect token', function(done) { 13 | request(app) 14 | .post('/token') 15 | .set('Authorization', 'Basic ' + new Buffer(data.clients[0].id + ':' + data.clients[0].secret, 'ascii').toString('base64')) 16 | .send({grant_type: 'password', username: data.users[0].username, password: data.users[0].password}) 17 | .expect(200, /refresh_token/) 18 | .end(function(err, res) { 19 | if (err) return done(err); 20 | refreshToken = res.body.refresh_token; 21 | accessToken = res.body.access_token; 22 | done(); 23 | }); 24 | }); 25 | 26 | it('POST /token with grant_type="refresh_token" expect same accessToken', function(done) { 27 | request(app) 28 | .post('/token') 29 | .set('Authorization', 'Basic ' + new Buffer(data.clients[0].id + ':' + data.clients[0].secret, 'ascii').toString('base64')) 30 | .send({grant_type: 'refresh_token', refresh_token: refreshToken}) 31 | .expect(200, /access_token/) 32 | .end(function(err, res) { 33 | if (err) 34 | done(err); 35 | else if (accessToken != res.body.access_token) 36 | done(new Error('AccessToken strings do not match. Expected=['+accessToken+'] Result=['+res.body.access_token+']')); 37 | else 38 | done(); 39 | }); 40 | }); 41 | 42 | it('POST /secure expect authorized', function(done) { 43 | request(app) 44 | .get('/secure') 45 | .set('Authorization', 'Bearer ' + accessToken) 46 | .expect(200, new RegExp(data.users[0].id, 'i'), done); 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /lib/controller/authorization/implicit.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | error = require('./../../error'), 4 | response = require('./../../util/response.js'), 5 | emitter = require('./../../events'); 6 | 7 | // @todo: move decision var to config 8 | // @todo: add state 9 | 10 | module.exports = function(req, res, client, scope, user, redirectUri) { 11 | 12 | var 13 | accessTokenValue; 14 | 15 | async.waterfall([ 16 | // Check user decision 17 | function(cb) { 18 | if (!req.body || typeof(req.body['decision']) == 'undefined') 19 | cb(new error.invalidRequest('No decision parameter passed')); 20 | else if (req.body['decision'] == 0) 21 | cb(new error.accessDenied('User denied the access to the resource')); 22 | else { 23 | req.oauth2.logger.debug('Decision check passed'); 24 | cb(); 25 | } 26 | }, 27 | // Generate new accessToken and save it 28 | function(cb) { 29 | req.oauth2.model.accessToken.create(req.oauth2.model.user.getId(user), req.oauth2.model.client.getId(client), scope, req.oauth2.model.accessToken.ttl, function(err, data) { 30 | if (err) 31 | cb(new error.serverError('Failed to call accessToken::save method')); 32 | else { 33 | accessTokenValue = data; 34 | req.oauth2.logger.debug('Access token saved: ', accessTokenValue); 35 | cb(); 36 | } 37 | }); 38 | } 39 | ], 40 | function(err) { 41 | if (err) response.error(req, res, err, redirectUri); 42 | else { 43 | var responseObj = { 44 | token_type: "bearer", 45 | access_token: accessTokenValue, 46 | expires_in: req.oauth2.model.accessToken.ttl 47 | }; 48 | emitter.authorization_implicit_granted(req, responseObj); 49 | response.data(req, res, responseObj, redirectUri, true); 50 | } 51 | }); 52 | }; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/oauth2/accessToken.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | RethinkDb = require('rethinkdb'), 3 | connection = require('./../connection.js'); 4 | 5 | var TABLE = 'access_token'; 6 | 7 | module.exports.getToken = function(accessToken) { 8 | return accessToken.token; 9 | }; 10 | 11 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 12 | var token = crypto.randomBytes(64).toString('hex'); 13 | var obj = {token: token, userId: userId, clientId: clientId, scope: scope, ttl: new Date().getTime() + ttl * 1000}; 14 | connection.acquire(function(err, conn) { 15 | if (err) cb(err); 16 | else RethinkDb.table(TABLE).insert(obj, {}).run(conn, function(err) { 17 | cb(err, token); 18 | }); 19 | }); 20 | }; 21 | 22 | module.exports.fetchByToken = function(token, cb) { 23 | connection.acquire(function(err, conn) { 24 | if (err) cb(err); 25 | else { 26 | RethinkDb.table(TABLE).filter({ token: token }).run(conn, function(err, cursor) { 27 | if (err) cb(err); 28 | else cursor.next(cb); 29 | }); 30 | } 31 | }); 32 | }; 33 | 34 | module.exports.checkTTL = function(accessToken) { 35 | return (accessToken.ttl > new Date().getTime()); 36 | }; 37 | 38 | module.exports.getTTL = function(accessToken, cb) { 39 | var ttl = moment(accessToken.ttl).diff(new Date(),'seconds'); 40 | return cb(null, ttl>0?ttl:0); 41 | }; 42 | 43 | module.exports.fetchByUserIdClientId = function(userId, clientId, cb) { 44 | var where = RethinkDb.and( 45 | RethinkDb.row('userId').eq(userId), 46 | RethinkDb.row('clientId').eq(clientId), 47 | RethinkDb.row('ttl').gt(new Date().getTime()) 48 | ); 49 | connection.acquire(function(err, conn) { 50 | if (err) cb(err); 51 | else { 52 | RethinkDb.table(TABLE).filter(where).orderBy(RethinkDb.desc('ttl')).limit(1).run(conn, function(err, cursor) { 53 | if (err) cb(err); 54 | else cursor.next(cb); 55 | }); 56 | } 57 | }); 58 | }; -------------------------------------------------------------------------------- /lib/middleware/bearer.js: -------------------------------------------------------------------------------- 1 | var 2 | response = require('./../util/response.js'), 3 | error = require('./../error/'), 4 | emitter = require('../events'); 5 | 6 | // @todo: add options for HMAC, force HEADER token, no errors parsing 7 | module.exports = function (req, res, next) { 8 | 9 | req.oauth2.logger.debug('Invoking bearer token parser middleware'); 10 | var token; 11 | 12 | // Look for token in header 13 | if (req.headers.authorization) { 14 | var pieces = req.headers.authorization.split(' ', 2); 15 | // Check auth header 16 | if (!pieces || pieces.length !== 2) 17 | return response.error(req, res, new error.accessDenied('Wrong authorization header')); 18 | // Only bearer auth is supported 19 | if (pieces[0].toLowerCase() != 'bearer') 20 | return response.error(req, res, new error.accessDenied('Unsupported authorization method header')); 21 | token = pieces[1]; 22 | req.oauth2.logger.debug('Bearer token parsed from authorization header: ', token); 23 | } 24 | // Look for token in query string 25 | else if (req.query && req.query['access_token']) { 26 | token = req.query['access_token']; 27 | req.oauth2.logger.debug('Bearer token parsed from query params: ', token); 28 | } 29 | // Look for token in post body 30 | else if (req.body && req.body['access_token']) { 31 | token = req.body['access_token']; 32 | req.oauth2.logger.debug('Bearer token parsed from body params: ', token); 33 | } 34 | // Not found 35 | else 36 | return response.error(req, res, new error.accessDenied('Bearer token not found')); 37 | 38 | // Try to fetch access token 39 | req.oauth2.model.accessToken.fetchByToken(token, function(err, object) { 40 | if (err) 41 | response.error(req, res, err); 42 | else if (!object) { 43 | response.error(req, res, new error.forbidden('Token not found or expired')); 44 | } 45 | else if (!req.oauth2.model.accessToken.checkTTL(object)) { 46 | response.error(req, res, new error.forbidden('Token already expired')) 47 | } 48 | else { 49 | emitter.access_token_fetched(req, object); 50 | req.oauth2.accessToken = object; 51 | req.oauth2.logger.debug('AccessToken fetched: ', object); 52 | next(); 53 | }; 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /lib/model/code.js: -------------------------------------------------------------------------------- 1 | var 2 | error = require('./../error'); 3 | 4 | /** 5 | * Typical code schema: 6 | * userId: { type: "object", required: true }, 7 | * clientId: { type: "object", required: true }, 8 | * code: { type: "string", required: true, unique: true }, 9 | * scope: { type: "array", required: false, 10 | * items: { type: "string", enum: ["possible", "scope", "values"] }, 11 | * } 12 | * 13 | * Primary key: code 14 | * Unique key: userId + clientId pair should be unique 15 | */ 16 | 17 | /** 18 | * Get userId parameter 19 | * 20 | * @param code {Object} Code object 21 | */ 22 | module.exports.getUserId = function(code) { 23 | throw new error.serverError('Code model method "getUserId" is not implemented'); 24 | }; 25 | 26 | /** 27 | * Get clientId parameter 28 | * 29 | * @param code {Object} Code object 30 | */ 31 | module.exports.getClientId = function(code) { 32 | throw new error.serverError('Code model method "getClientId" is not implemented'); 33 | }; 34 | 35 | /** 36 | * Get scope parameter 37 | * 38 | * @param code {Object} Code object 39 | */ 40 | module.exports.getScope = function(code) { 41 | throw new error.serverError('Code model method "getScope" is not implemented'); 42 | }; 43 | 44 | /** 45 | * Fetches accessToken object by token 46 | * Should be implemented with server logic 47 | * 48 | * Remember to check ttl if ttl is saved in object (if ttl is not valid return null) 49 | * 50 | * @param code {String} Unique identifier 51 | * @param cb {Function} Function callback ->(error, object) 52 | */ 53 | module.exports.fetchByCode = function(code, cb) { 54 | throw new error.serverError('Code model method "fetchByCode" is not implemented'); 55 | }; 56 | 57 | /** 58 | * Create code object (generate + save) 59 | * Should be implemented with server logic 60 | * 61 | * @param userId {String} Unique identifier 62 | * @param clientId {String} Unique identifier 63 | * @param scope {Array|null} Scope values 64 | * @param ttl {Number} Time to live in seconds 65 | * @param cb {Function} Function callback ->(error, code{String}) 66 | */ 67 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 68 | throw new error.serverError('Code model method "create" is not implemented'); 69 | }; 70 | 71 | /** 72 | * Remove code object (already used) 73 | * Should be implemented with server logic 74 | * 75 | * @param code {String} Generated code string 76 | * @param cb {Function} Function callback ->(error) 77 | */ 78 | module.exports.removeByCode = function(code, cb) { 79 | throw new error.serverError('Code model method "removeByCode" is not implemented'); 80 | }; 81 | 82 | /** 83 | * Access token time to live 84 | * @type {Number} Seconds 85 | */ 86 | module.exports.ttl = 300; -------------------------------------------------------------------------------- /test/server/model/rethinkdb/data.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | RethinkDb = require('rethinkdb'), 3 | connection = require('./connection.js'), 4 | config = require('./config.js'), 5 | data = require('./../data.js'); 6 | 7 | module.exports.initialize = function(cb) { 8 | 9 | var conn, 10 | tables; 11 | 12 | async.series([ 13 | // Connection 14 | function(cb) { 15 | connection.acquire(function(err, c) { 16 | if (err) cb(err); 17 | else { 18 | conn = c; 19 | cb(); 20 | } 21 | }); 22 | }, 23 | // Create DB 24 | function(cb) { 25 | RethinkDb.dbList().run(conn, function(err, dbList) { 26 | if (err) 27 | cb(err); 28 | else if (dbList.indexOf(config.db) != -1) 29 | cb(); 30 | else 31 | RethinkDb.dbCreate(config.db).run(conn, cb); 32 | }); 33 | }, 34 | // Get tables 35 | function(cb) { 36 | RethinkDb.db(config.db).tableList().run(conn, function(err, data) { 37 | if (err) cb(err); 38 | else { 39 | tables = data; 40 | cb(); 41 | } 42 | }); 43 | }, 44 | // Create missing tables 45 | function(cb) { 46 | async.eachSeries([ 'access_token', 'refresh_token', 'authorization_code', 'client', 'user' ], function(t, cb) { 47 | if (tables.indexOf(t) != -1) return cb(); 48 | 49 | RethinkDb.db(config.db).tableCreate(t, {}).run(conn, cb); 50 | }, cb); 51 | }, 52 | // Replace users data 53 | function(cb) { 54 | async.eachSeries(data.users, function(obj, cb) { 55 | RethinkDb.table('user').get(obj.id).replace(obj).run(conn, cb); 56 | }, cb); 57 | }, 58 | // Replace clients data 59 | function(cb) { 60 | async.eachSeries(data.clients, function(obj, cb) { 61 | RethinkDb.table('client').get(obj.id).replace(obj).run(conn, cb); 62 | }, cb); 63 | } 64 | ], cb); 65 | 66 | }; 67 | 68 | if (require.main == module) { 69 | module.exports.initialize(function(err) { 70 | if (err) console.error(err); 71 | else { 72 | console.log('Data initialized'); 73 | console.log('Closing connection to RethinkDB'); 74 | connection.close(function(err) { 75 | if (err) console.error(err); 76 | else console.log('Finished'); 77 | }); 78 | } 79 | }); 80 | } -------------------------------------------------------------------------------- /test/implicit.js: -------------------------------------------------------------------------------- 1 | var 2 | query = require('querystring'), 3 | request = require('supertest'), 4 | data = require('./server/model/data.js'), 5 | app = require('./server/app.js'); 6 | 7 | describe('Implicit Grant Type ',function() { 8 | 9 | var 10 | loginUrl, 11 | authorizationUrl, 12 | cookie, 13 | accessToken; 14 | 15 | var cookiePattern = new RegExp('connect.sid=(.*?);'); 16 | 17 | it('GET /authorization with response_type="token" expect login form redirect', function(done) { 18 | request(app) 19 | .get('/authorization?' + query.stringify({ 20 | redirect_uri: data.clients[1].redirectUri, 21 | client_id: data.clients[1].id, 22 | response_type: 'token' 23 | })) 24 | .expect('Location', new RegExp('login')) 25 | .expect(302, function(err, res) { 26 | if (err) return done(err); 27 | loginUrl = res.headers.location; 28 | done(); 29 | }); 30 | }); 31 | 32 | it('POST /login authorize', function(done) { 33 | request(app) 34 | .post(loginUrl) 35 | .send({ username: data.users[0].username, password: data.users[0].password }) 36 | .expect('Location', new RegExp('authorization')) 37 | .expect(302, function(err, res) { 38 | if (err) return done(err); 39 | authorizationUrl = res.headers.location; 40 | cookie = cookiePattern.exec(res.headers['set-cookie'][0])[0]; 41 | done(); 42 | }); 43 | }); 44 | 45 | it('GET /authorize with response_type="token" expect decision', function(done) { 46 | request(app) 47 | .get(authorizationUrl) 48 | .set('Cookie', cookie) 49 | .expect(200, function(err, res) { 50 | if (err) return done(err); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('POST /authorize with response_type="token" and decision="1" expect code redirect', function(done) { 56 | request(app) 57 | .post(authorizationUrl) 58 | .send({ decision: 1 }) 59 | .set('Cookie', cookie) 60 | .expect(302, function(err, res) { 61 | if (err) return done(err); 62 | 63 | var uri = res.headers.location; 64 | if (uri.indexOf('#') == -1) return done(new Error('Failed to parse redirect uri')); 65 | var q = query.parse(uri.substr(uri.indexOf('#') + 1)); 66 | if (!q['access_token']) return done(new Error('No code value found in redirect uri')); 67 | 68 | accessToken = q['access_token']; 69 | done(); 70 | }) 71 | }); 72 | 73 | it('POST /secure expect authorized', function(done) { 74 | request(app) 75 | .get('/secure') 76 | .set('Authorization', 'Bearer ' + accessToken) 77 | .expect(200, new RegExp(data.users[0].id, 'i'), done); 78 | }); 79 | 80 | 81 | }); -------------------------------------------------------------------------------- /lib/model/accessToken.js: -------------------------------------------------------------------------------- 1 | var 2 | error = require('./../error'); 3 | 4 | /** 5 | * Typical accessToken schema: 6 | * userId: { type: "object", required: true }, 7 | * clientId: { type: "object", required: true }, 8 | * token: { type: "string", required: true, unique: true }, 9 | * scope: { type: "array", required: false, 10 | * items: { type: "string", enum: ["possible", "scope", "values"] }, 11 | * } 12 | * 13 | * Primary key: token 14 | * @todo: CHECK IT, seems no need to be unique 15 | * Unique key: userId + clientId pair should be unique 16 | */ 17 | 18 | /** 19 | * Gets token of the accessToken 20 | * 21 | * @param accessToken {Object} accessToken object 22 | */ 23 | module.exports.getToken = function(accessToken) { 24 | throw new error.serverError('accessToken model method "getToken" is not implemented'); 25 | }; 26 | 27 | /** 28 | * Fetches accessToken object by token 29 | * Should be implemented with server logic 30 | * 31 | * Remember to check ttl if ttl is saved in object (if ttl is not valid return null) 32 | * 33 | * @param token {String} Unique identifier 34 | * @param cb {Function} Function callback ->(error, object) 35 | */ 36 | module.exports.fetchByToken = function(token, cb) { 37 | throw new error.serverError('accessToken model method "fetchByToken" is not implemented'); 38 | }; 39 | 40 | /** 41 | * Fetches accessToken object by userId-clientId pair 42 | * Should be implemented with server logic 43 | * 44 | * @param userId {String} Unique identifier 45 | * @param clientId {String} Unique identifier 46 | * @param cb {Function} Function callback ->(error, object) 47 | */ 48 | module.exports.fetchByUserIdClientId = function(userId, clientId, cb) { 49 | throw new error.serverError('accessToken model method "fetchByUserIdClientId" is not implemented'); 50 | }; 51 | 52 | /** 53 | * Check if accessToken is valid and not expired 54 | * 55 | * @param accessToken 56 | */ 57 | module.exports.checkTTL = function(accessToken) { 58 | throw new error.serverError('accessToken model method "checkTTL" is not implemented'); 59 | }; 60 | 61 | /** 62 | * Get TTL from accessToken to deliver it to the client 63 | * when this does the refresh token flow and the access token is not expired 64 | * 65 | * @param accessToken 66 | */ 67 | module.exports.getTTL = function(accessToken, cb) { 68 | throw new error.serverError('accessToken model method "getTTL" is not implemented'); 69 | }; 70 | 71 | /** 72 | * Create accessToken object (generate + save) 73 | * Should be implemented with server logic 74 | * 75 | * @param userId {String} Unique identifier 76 | * @param clientId {String} Unique identifier 77 | * @param scope {Array|null} Scope values 78 | * @param ttl {Number} Time to live in seconds 79 | * @param cb {Function} Function callback ->(error, token{String}) 80 | */ 81 | module.exports.create = function(userId, clientId, scope, ttl, cb) { 82 | throw new error.serverError('accessToken model method "create" is not implemented'); 83 | }; 84 | 85 | /** 86 | * Access token time to live 87 | * @type {Number} Seconds 88 | */ 89 | module.exports.ttl = 3600; -------------------------------------------------------------------------------- /lib/model/refreshToken.js: -------------------------------------------------------------------------------- 1 | var 2 | error = require('./../error'); 3 | 4 | /** 5 | * Typical refreshToken schema: 6 | * userId: { type: "object", required: true }, 7 | * clientId: { type: "object", required: true }, 8 | * token: { type: "string", required: true, unique: true }, 9 | * scope: { type: "array", required: false, 10 | * items: { type: "string", enum: ["possible", "scope", "values"] }, 11 | * } 12 | * 13 | * Primary key: token 14 | * Unique key: userId + clientId pair should be unique 15 | */ 16 | 17 | /** 18 | * Gets userId parameter of the refreshToken 19 | * 20 | * @param refreshToken {Object} RefreshToken object 21 | */ 22 | module.exports.getUserId = function(refreshToken) { 23 | throw new error.serverError('RefreshToken model method "getUserId" is not implemented'); 24 | }; 25 | 26 | /** 27 | * Gets clientId parameter of the refreshToken 28 | * 29 | * @param refreshToken {Object} RefreshToken object 30 | */ 31 | module.exports.getClientId = function(refreshToken) { 32 | throw new error.serverError('RefreshToken model method "getClientId" is not implemented'); 33 | }; 34 | 35 | /** 36 | * Gets scope parameter of the refreshToken 37 | * 38 | * @param refreshToken {Object} RefreshToken object 39 | */ 40 | module.exports.getScope = function(refreshToken) { 41 | throw new error.serverError('RefreshToken model method "getScope" is not implemented'); 42 | }; 43 | 44 | /** 45 | * Fetches refreshToken object by token 46 | * Should be implemented with server logic 47 | * 48 | * @param token {String} Unique identifier 49 | * @param cb {Function} Function callback ->(error, object) 50 | */ 51 | module.exports.fetchByToken = function(token, cb) { 52 | throw new error.serverError('RefreshToken model method "fetchByToken" is not implemented'); 53 | }; 54 | 55 | /** 56 | * Removes refreshToken (revokes) for the client-user pair 57 | * Should be implemented with server logic 58 | * 59 | * @param userId {String} Unique identifier 60 | * @param clientId {String} Unique identifier 61 | * @param cb {Function} Function callback ->(error) 62 | */ 63 | module.exports.removeByUserIdClientId = function(userId, clientId, cb) { 64 | throw new error.serverError('RefreshToken model method "removeByUserIdClientId" is not implemented'); 65 | }; 66 | 67 | /** 68 | * Removes refreshToken (revokes) by token value 69 | * Should be implemented with server logic 70 | * 71 | * @param refreshToken {String} Unique identifier 72 | * @param cb {Function} Function callback ->(error) 73 | */ 74 | module.exports.removeByRefreshToken = function(refreshToken, cb) { 75 | throw new error.serverError('RefreshToken model method "removeByRefreshToken" is not implemented'); 76 | }; 77 | 78 | /** 79 | * Create refreshToken object (generate + save) 80 | * Should be implemented with server logic 81 | * 82 | * @param userId {String} Unique identifier 83 | * @param clientId {String} Unique identifier 84 | * @param scope {Array|null} Scope values 85 | * @param cb {Function} Function callback ->(error, token{String}) 86 | */ 87 | module.exports.create = function(userId, clientId, scope, cb) { 88 | throw new error.serverError('RefreshToken model method "create" is not implemented'); 89 | }; -------------------------------------------------------------------------------- /test/server/oauth20.js: -------------------------------------------------------------------------------- 1 | var oauth20 = require('./../../lib'); 2 | 3 | // Define methods 4 | module.exports = function(type) { 5 | var obj = new oauth20({log: {level: 4}}); 6 | 7 | var model = require('./model/' + type).oauth2; 8 | if (!model) 9 | throw new Error('Unknown model type: ' + type); 10 | 11 | // Redefine oauth20 abstract methods 12 | 13 | // Set client methods 14 | obj.model.client.getId = model.client.getId; 15 | obj.model.client.getRedirectUri = model.client.getRedirectUri; 16 | obj.model.client.checkRedirectUri = model.client.checkRedirectUri; 17 | obj.model.client.fetchById = model.client.fetchById; 18 | obj.model.client.checkSecret = model.client.checkSecret; 19 | 20 | // User 21 | obj.model.user.getId = model.user.getId; 22 | obj.model.user.fetchById = model.user.fetchById; 23 | obj.model.user.fetchByUsername = model.user.fetchByUsername; 24 | obj.model.user.fetchFromRequest = model.user.fetchFromRequest; 25 | obj.model.user.checkPassword = model.user.checkPassword; 26 | 27 | // Refresh token 28 | obj.model.refreshToken.getUserId = model.refreshToken.getUserId; 29 | obj.model.refreshToken.getClientId = model.refreshToken.getClientId; 30 | obj.model.refreshToken.getScope = model.refreshToken.getScope; 31 | obj.model.refreshToken.fetchByToken = model.refreshToken.fetchByToken; 32 | obj.model.refreshToken.removeByUserIdClientId = model.refreshToken.removeByUserIdClientId; 33 | obj.model.refreshToken.removeByRefreshToken = model.refreshToken.removeByRefreshToken; 34 | obj.model.refreshToken.create = model.refreshToken.create; 35 | 36 | // Access token 37 | obj.model.accessToken.getToken = model.accessToken.getToken; 38 | obj.model.accessToken.fetchByToken = model.accessToken.fetchByToken; 39 | obj.model.accessToken.checkTTL = model.accessToken.checkTTL; 40 | obj.model.accessToken.getTTL = model.accessToken.getTTL; 41 | obj.model.accessToken.fetchByUserIdClientId = model.accessToken.fetchByUserIdClientId; 42 | obj.model.accessToken.create = model.accessToken.create; 43 | 44 | // Code 45 | obj.model.code.create = model.code.create; 46 | obj.model.code.fetchByCode = model.code.fetchByCode; 47 | obj.model.code.removeByCode = model.code.removeByCode; 48 | obj.model.code.getUserId = model.code.getUserId; 49 | obj.model.code.getClientId = model.code.getClientId; 50 | obj.model.code.getScope = model.code.getScope; 51 | obj.model.code.checkTTL = model.code.getScope; 52 | 53 | // Decision controller 54 | obj.decision = function(req, res, client, scope, user) { 55 | var html = [ 56 | 'Currently your are logged with id = ' + req.oauth2.model.user.getId(user), 57 | 'Client with id ' + req.oauth2.model.client.getId(client) + ' asks for access', 58 | 'Scope asked ' + scope.join(), 59 | '
', 60 | '', 61 | '', 62 | '
', 63 | '
', 64 | '', 65 | '', 66 | '
' 67 | ]; 68 | res.send(html.join('
')); 69 | }; 70 | 71 | return obj; 72 | }; 73 | 74 | -------------------------------------------------------------------------------- /test/refreshToken.js: -------------------------------------------------------------------------------- 1 | var 2 | request = require('supertest'), 3 | data = require('./server/model/data.js'), 4 | app = require('./server/app.js'); 5 | 6 | describe('Refresh Token Grant Type ',function() { 7 | 8 | this.timeout(3000); 9 | 10 | before(function() { 11 | app.get('oauth2').model.accessToken.ttl = 2; 12 | }); 13 | 14 | after(function(){ 15 | app.get('oauth2').model.accessToken.ttl = 3600; 16 | }); 17 | 18 | var 19 | refreshToken, 20 | accessToken, 21 | newAccessToken; 22 | 23 | it('POST /token with grant_type="password" expect token', function(done) { 24 | request(app) 25 | .post('/token') 26 | .set('Authorization', 'Basic ' + new Buffer(data.clients[2].id + ':' + data.clients[2].secret, 'ascii').toString('base64')) 27 | .send({grant_type: 'password', username: data.users[0].username, password: data.users[0].password}) 28 | .expect(200, /refresh_token/) 29 | .end(function(err, res) { 30 | if (err) return done(err); 31 | refreshToken = res.body.refresh_token; 32 | accessToken = res.body.access_token; 33 | done(); 34 | }); 35 | }); 36 | 37 | it('POST /token with grant_type="refresh_token" expect same accessToken', function(done) { 38 | request(app) 39 | .post('/token') 40 | .set('Authorization', 'Basic ' + new Buffer(data.clients[2].id + ':' + data.clients[2].secret, 'ascii').toString('base64')) 41 | .send({grant_type: 'refresh_token', refresh_token: refreshToken}) 42 | .expect(200, /access_token/) 43 | .end(function(err, res) { 44 | if (err) 45 | done(err); 46 | else if (accessToken != res.body.access_token) 47 | done(new Error('AccessToken strings do not match. Expected=['+accessToken+'] Result=['+res.body.access_token+']')); 48 | else 49 | done(); 50 | }); 51 | }); 52 | 53 | it('Wait and POST /token with grant_type="refresh_token" expect diferent [new]accessToken', function(done) { 54 | setTimeout(function() { 55 | request(app) 56 | .post('/token') 57 | .set('Authorization', 'Basic ' + new Buffer(data.clients[2].id + ':' + data.clients[2].secret, 'ascii').toString('base64')) 58 | .send({grant_type: 'refresh_token', refresh_token: refreshToken}) 59 | .expect(200, /access_token/) 60 | .end(function (err, res) { 61 | if (err) 62 | done(err); 63 | else if (accessToken == res.body.access_token) 64 | done(new Error('AccessToken strings do match. Expected=[' + accessToken + '] Result=[' + res.body.access_token + ']')); 65 | else{ 66 | newAccessToken = res.body.access_token; 67 | done(); 68 | } 69 | }); 70 | },2000); 71 | }); 72 | 73 | it('POST /secure with old token expect forbidden', function(done) { 74 | request(app) 75 | .get('/secure') 76 | .set('Authorization', 'Bearer ' + accessToken) 77 | .expect(403, /forbidden/, done); 78 | }); 79 | 80 | it('POST /secure witj new token expect authorized', function(done) { 81 | request(app) 82 | .get('/secure') 83 | .set('Authorization', 'Bearer ' + newAccessToken) 84 | .expect(200, new RegExp(data.users[0].id, 'i'), done); 85 | }); 86 | 87 | }); -------------------------------------------------------------------------------- /test/authorizationCode.js: -------------------------------------------------------------------------------- 1 | var 2 | query = require('querystring'), 3 | request = require('supertest'), 4 | data = require('./server/model/data.js'), 5 | app = require('./server/app.js'); 6 | 7 | describe('Authorization Code Grant Type ',function() { 8 | 9 | var 10 | loginUrl, 11 | authorizationUrl, 12 | cookie, 13 | code, 14 | accessToken; 15 | 16 | var cookiePattern = new RegExp('connect.sid=(.*?);'); 17 | 18 | it('GET /authorization with response_type="code" expect login form redirect', function(done) { 19 | request(app) 20 | .get('/authorization?' + query.stringify({ 21 | redirect_uri: data.clients[1].redirectUri, 22 | client_id: data.clients[1].id, 23 | response_type: 'code' 24 | })) 25 | .expect('Location', new RegExp('login')) 26 | .expect(302, function(err, res) { 27 | if (err) return done(err); 28 | loginUrl = res.headers.location; 29 | done(); 30 | }); 31 | }); 32 | 33 | it('POST /login expect authorized', function(done) { 34 | request(app) 35 | .post(loginUrl) 36 | .send({ username: data.users[0].username, password: data.users[0].password }) 37 | .expect('Location', new RegExp('authorization')) 38 | .expect(302, function(err, res) { 39 | if (err) return done(err); 40 | authorizationUrl = res.headers.location; 41 | cookie = cookiePattern.exec(res.headers['set-cookie'][0])[0]; 42 | done(); 43 | }); 44 | }); 45 | 46 | it('GET /authorize with response_type="code" expect decision', function(done) { 47 | request(app) 48 | .get(authorizationUrl) 49 | .set('Cookie', cookie) 50 | .expect(200, function(err, res) { 51 | if (err) return done(err); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('POST /authorize with response_type="code" and decision="1" expect code redirect', function(done) { 57 | request(app) 58 | .post(authorizationUrl) 59 | .send({ decision: 1 }) 60 | .set('Cookie', cookie) 61 | .expect(302, function(err, res) { 62 | if (err) return done(err); 63 | 64 | var uri = res.headers.location; 65 | if (uri.indexOf('?') == -1) return done(new Error('Failed to parse redirect uri')); 66 | var q = query.parse(uri.substr(uri.indexOf('?') + 1)); 67 | if (!q['code']) return done(new Error('No code value found in redirect uri')); 68 | 69 | code = q['code']; 70 | done(); 71 | }) 72 | }); 73 | 74 | it('POST /token with grant_type="authorization_code" expect token', function(done) { 75 | request(app) 76 | .post('/token') 77 | .set('Authorization', 'Basic ' + new Buffer(data.clients[1].id + ':' + data.clients[1].secret, 'ascii').toString('base64')) 78 | .send({grant_type: 'authorization_code', code: code, redirectUri: data.clients[1].redirectUri}) 79 | .expect(200, /refresh_token/) 80 | .end(function(err, res) { 81 | if (err) return done(err); 82 | accessToken = res.body.access_token; 83 | done(); 84 | }); 85 | }); 86 | 87 | it('POST /secure expect authorized', function(done) { 88 | request(app) 89 | .get('/secure') 90 | .set('Authorization', 'Bearer ' + accessToken) 91 | .expect(200, new RegExp(data.users[0].id, 'i'), done); 92 | }); 93 | 94 | }); -------------------------------------------------------------------------------- /test/server/app.js: -------------------------------------------------------------------------------- 1 | // Run tests via "npm --type=TYPE test" (types available: memory (default), redis are available) 2 | var TYPE = process.env['npm_config_type'] || 'memory'; 3 | 4 | var 5 | query = require('querystring'), 6 | express = require('express'), 7 | cookieParser = require('cookie-parser'), 8 | session = require('express-session'), 9 | bodyParser = require('body-parser'); 10 | 11 | var 12 | config = require('./config.js'), 13 | server = express(), 14 | oauth20 = require('./oauth20.js')(TYPE), 15 | model = require('./model/' + TYPE); 16 | 17 | // Configuration for renewing refresh token in refresh token flow 18 | oauth20.renewRefreshToken = true; 19 | 20 | server.set('oauth2', oauth20); 21 | 22 | // Middleware 23 | server.use(cookieParser()); 24 | server.use(session({ secret: 'oauth20-provider-test-server', resave: false, saveUninitialized: false })); 25 | server.use(bodyParser.urlencoded({extended: false})); 26 | server.use(bodyParser.json()); 27 | server.use(oauth20.inject()); 28 | 29 | // View 30 | server.set('views', './view'); 31 | server.set('view engine', 'jade'); 32 | 33 | // Middleware. User authorization 34 | function isUserAuthorized(req, res, next) { 35 | if (req.session.authorized) next(); 36 | else { 37 | var params = req.query; 38 | params.backUrl = req.path; 39 | res.redirect('/login?' + query.stringify(params)); 40 | } 41 | } 42 | 43 | // Define OAuth2 Authorization Endpoint 44 | server.get('/authorization', isUserAuthorized, oauth20.controller.authorization, function(req, res) { 45 | res.render('authorization', { layout: false }); 46 | }); 47 | server.post('/authorization', isUserAuthorized, oauth20.controller.authorization); 48 | 49 | // Define OAuth2 Token Endpoint 50 | server.post('/token', oauth20.controller.token); 51 | 52 | // Define user login routes 53 | server.get('/login', function(req, res) { 54 | res.render('login', {layout: false}); 55 | }); 56 | 57 | server.post('/login', function(req, res, next) { 58 | var backUrl = req.query.backUrl ? req.query.backUrl : '/'; 59 | delete(req.query.backUrl); 60 | backUrl += backUrl.indexOf('?') > -1 ? '&' : '?'; 61 | backUrl += query.stringify(req.query); 62 | 63 | // Already logged in 64 | if (req.session.authorized) res.redirect(backUrl); 65 | // Trying to log in 66 | else if (req.body.username && req.body.password) { 67 | model.oauth2.user.fetchByUsername(req.body.username, function(err, user) { 68 | if (err) next(err); 69 | else { 70 | model.oauth2.user.checkPassword(user, req.body.password, function(err, valid) { 71 | if (err) next(err); 72 | else if (!valid) res.redirect(req.url); 73 | else { 74 | req.session.user = user; 75 | req.session.authorized = true; 76 | res.redirect(backUrl); 77 | } 78 | }); 79 | } 80 | }); 81 | } 82 | // Please login 83 | else res.redirect(req.url); 84 | }); 85 | 86 | // Some secure method 87 | server.get('/secure', oauth20.middleware.bearer, function(req, res) { 88 | if (!req.oauth2.accessToken) return res.status(403).send('Forbidden'); 89 | if (!req.oauth2.accessToken.userId) return res.status(403).send('Forbidden'); 90 | res.send('Hi! Dear user ' + req.oauth2.accessToken.userId + '!'); 91 | }); 92 | 93 | // Some secure client method 94 | server.get('/client', oauth20.middleware.bearer, function(req, res) { 95 | if (!req.oauth2.accessToken) return res.status(403).send('Forbidden'); 96 | res.send('Hi! Dear client ' + req.oauth2.accessToken.clientId + '!'); 97 | }); 98 | 99 | // Expose functions 100 | var start = module.exports.start = function() { 101 | server.listen(config.server.port, config.server.host, function(err) { 102 | if (err) console.error(err); 103 | else console.log('Server started at ' + config.server.host + ':' + config.server.port); 104 | }); 105 | }; 106 | 107 | module.exports = server; 108 | 109 | if (require.main == module) { 110 | start(); 111 | } -------------------------------------------------------------------------------- /lib/controller/token/authorizationCode.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | error = require('./../../error'); 4 | 5 | module.exports = function(oauth2, client, sCode, redirectUri, pCb) { 6 | 7 | // Define variables 8 | var responseObj = { 9 | token_type: "bearer" 10 | }; 11 | var code; 12 | 13 | async.waterfall([ 14 | // Fetch code 15 | function(cb) { 16 | oauth2.model.code.fetchByCode(sCode, function(err, obj) { 17 | if (err) 18 | cb(new error.serverError('Failed to call code::fetchByCode method')); 19 | else if (!obj) 20 | cb(new error.invalidGrant('Code not found')); 21 | else if (oauth2.model.code.getClientId(obj) != oauth2.model.client.getId(client)) 22 | cb(new error.invalidGrant('Code is issued by another client')); 23 | else if (!oauth2.model.code.checkTTL(obj)) 24 | cb(new error.invalidGrant('Code is already expired')); 25 | else { 26 | oauth2.logger.debug('Code fetched: ', obj); 27 | code = obj; 28 | cb(); 29 | } 30 | }); 31 | }, 32 | // @todo: clarify. Check redirectUri? Weird standard, why should we? 33 | // Remove old refreshToken (if exists) with userId-clientId pair 34 | function(cb) { 35 | oauth2.model.refreshToken.removeByUserIdClientId(oauth2.model.code.getUserId(code), oauth2.model.code.getClientId(code), function(err) { 36 | if (err) 37 | cb(new error.serverError('Failed to call refreshToken::removeByUserIdClientId method')); 38 | else { 39 | oauth2.logger.debug('Refresh token removed'); 40 | cb(); 41 | } 42 | }); 43 | }, 44 | // Generate new refreshToken and save it 45 | function(cb) { 46 | //check if client has grant type refresh_token, if not, it will not be including in response (short time authorization) 47 | if(!oauth2.model.client.checkGrantType(client, 'refresh_token')){ 48 | oauth2.logger.debug('Client has not the grant type refresh_token, skip creation'); 49 | return cb(); 50 | } 51 | 52 | oauth2.model.refreshToken.create(oauth2.model.code.getUserId(code), oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code), function(err, data) { 53 | if (err) 54 | cb(new error.serverError('Failed to call refreshToken::save method')); 55 | else { 56 | responseObj.refresh_token = data; 57 | oauth2.logger.debug('Refresh token saved: ', responseObj.refresh_token); 58 | cb(); 59 | } 60 | }); 61 | }, 62 | // Generate new accessToken and save it 63 | function(cb) { 64 | oauth2.model.accessToken.create(oauth2.model.code.getUserId(code), oauth2.model.code.getClientId(code), oauth2.model.code.getScope(code), oauth2.model.accessToken.ttl, function(err, data) { 65 | if (err) 66 | cb(new error.serverError('Failed to call accessToken::save method')); 67 | else { 68 | responseObj.access_token = data; 69 | responseObj.expires_in = oauth2.model.accessToken.ttl; 70 | oauth2.logger.debug('Access token saved: ', responseObj.access_token); 71 | cb(); 72 | } 73 | }); 74 | }, 75 | // Remove used code 76 | function(cb) { 77 | oauth2.model.code.removeByCode(sCode, function(err) { 78 | if (err) 79 | cb(new error.serverError('Failed to call code::removeByCode method')); 80 | else { 81 | oauth2.logger.debug('Code removed'); 82 | cb(); 83 | } 84 | }); 85 | } 86 | ], function(err) { 87 | if (err) pCb(err); 88 | else { 89 | pCb(null, { event: 'token_granted_from_authorization_code', data:responseObj}); 90 | } 91 | }); 92 | 93 | }; -------------------------------------------------------------------------------- /test/authorizationCode_checkRefreshTokenGrant.js: -------------------------------------------------------------------------------- 1 | var 2 | query = require('querystring'), 3 | request = require('supertest'), 4 | data = require('./server/model/data.js'), 5 | app = require('./server/app.js'); 6 | 7 | describe('Authorization Code Grant Type without client\'s refresh token grant type',function() { 8 | 9 | before(function() { 10 | app.get('oauth2').model.client.checkGrantType = function(client, grant){ 11 | return grant != 'refresh_token'; 12 | }; 13 | }); 14 | 15 | after(function(){ 16 | app.get('oauth2').model.client.checkGrantType = function(client, grant){ 17 | return []; 18 | }; 19 | }); 20 | 21 | var 22 | loginUrl, 23 | authorizationUrl, 24 | cookie, 25 | code, 26 | accessToken; 27 | 28 | var cookiePattern = new RegExp('connect.sid=(.*?);'); 29 | 30 | it('GET /authorization with response_type="code" expect login form redirect', function(done) { 31 | request(app) 32 | .get('/authorization?' + query.stringify({ 33 | redirect_uri: data.clients[1].redirectUri, 34 | client_id: data.clients[1].id, 35 | response_type: 'code' 36 | })) 37 | .expect('Location', new RegExp('login')) 38 | .expect(302, function(err, res) { 39 | if (err) return done(err); 40 | loginUrl = res.headers.location; 41 | done(); 42 | }); 43 | }); 44 | 45 | it('POST /login expect authorized', function(done) { 46 | request(app) 47 | .post(loginUrl) 48 | .send({ username: data.users[0].username, password: data.users[0].password }) 49 | .expect('Location', new RegExp('authorization')) 50 | .expect(302, function(err, res) { 51 | if (err) return done(err); 52 | authorizationUrl = res.headers.location; 53 | cookie = cookiePattern.exec(res.headers['set-cookie'][0])[0]; 54 | done(); 55 | }); 56 | }); 57 | 58 | it('GET /authorize with response_type="code" expect decision', function(done) { 59 | request(app) 60 | .get(authorizationUrl) 61 | .set('Cookie', cookie) 62 | .expect(200, function(err, res) { 63 | if (err) return done(err); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('POST /authorize with response_type="code" and decision="1" expect code redirect', function(done) { 69 | request(app) 70 | .post(authorizationUrl) 71 | .send({ decision: 1 }) 72 | .set('Cookie', cookie) 73 | .expect(302, function(err, res) { 74 | if (err) return done(err); 75 | 76 | var uri = res.headers.location; 77 | if (uri.indexOf('?') == -1) return done(new Error('Failed to parse redirect uri')); 78 | var q = query.parse(uri.substr(uri.indexOf('?') + 1)); 79 | if (!q['code']) return done(new Error('No code value found in redirect uri')); 80 | 81 | code = q['code']; 82 | done(); 83 | }) 84 | }); 85 | 86 | it('POST /token with grant_type="authorization_code" expect token but not refresh_token', function(done) { 87 | request(app) 88 | .post('/token') 89 | .set('Authorization', 'Basic ' + new Buffer(data.clients[1].id + ':' + data.clients[1].secret, 'ascii').toString('base64')) 90 | .send({grant_type: 'authorization_code', code: code, redirectUri: data.clients[1].redirectUri}) 91 | .expect(200) 92 | .expect(function check_no_refresh_token(res){ 93 | if(res.body.refresh_token){ 94 | throw new Error('refresh_token received') 95 | } 96 | }) 97 | .end(function(err, res) { 98 | if (err) return done(err); 99 | accessToken = res.body.access_token; 100 | done(); 101 | }); 102 | }); 103 | 104 | it('POST /secure expect authorized', function(done) { 105 | request(app) 106 | .get('/secure') 107 | .set('Authorization', 'Bearer ' + accessToken) 108 | .expect(200, new RegExp(data.users[0].id, 'i'), done); 109 | }); 110 | 111 | }); -------------------------------------------------------------------------------- /lib/model/client.js: -------------------------------------------------------------------------------- 1 | var 2 | error = require('./../error'); 3 | 4 | /** 5 | * Typical client schema: 6 | * _id: { type: "object", required: true, unique: true }, 7 | * name: { type: "string", required: true }, 8 | * secret: { type: "string", required: true }, 9 | * uri: { type: "string", required: false }, 10 | * scope: { type: "array", required: false, 11 | * items: { type: "string", enum: ["possible", "scope", "values"] }, 12 | * }, 13 | * grants: { type: "array", required: false, 14 | * items: { type: "string", enum: ["authorization_code", "implicit", "password", "client_credentials"] } 15 | * } 16 | */ 17 | 18 | /** 19 | * Gets clients primary key 20 | * 21 | * @param client {Object} Client object 22 | */ 23 | module.exports.getId = function(client) { 24 | throw new error.serverError('Client model method "getId" is not implemented'); 25 | }; 26 | 27 | /** 28 | * Gets clients secret 29 | * 30 | * @param client {Object} Client object 31 | */ 32 | module.exports.getSecret = function(client) { 33 | throw new error.serverError('Client model method "getSecret" is not implemented'); 34 | }; 35 | 36 | /** 37 | * Gets clients redirect uri 38 | * 39 | * @param client {Object} Client object 40 | */ 41 | module.exports.getRedirectUri = function(client) { 42 | throw new error.serverError('Client model method "getRedirectUri" is not implemented'); 43 | }; 44 | 45 | /** 46 | * Checks redirect uri for the client 47 | * 48 | * @param client {Object} Client object 49 | * @param client {String} Redirect URI to be checked 50 | */ 51 | module.exports.checkRedirectUri = function(client, redirectUri) { 52 | /** 53 | * For example: 54 | * // for single redirect uri per client 55 | * return (redirectUri.indexOf(getRedirectUri(client)) === 0 && 56 | * redirectUri.replace(getRedirectUri(client), '').indexOf('#') === -1); 57 | * 58 | * // for multiple redirect uris per client 59 | * return (getRedirectUri(client).indexOf(redirectUri) !== -1); 60 | */ 61 | throw new error.serverError('Client model method "checkRedirectUri" is not implemented'); 62 | }; 63 | 64 | /** 65 | * Fetches client object by primary key 66 | * Should be implemented with server logic 67 | * 68 | * @param clientId {String} Unique identifier 69 | * @param cb {Function} Function callback ->(error, object) 70 | */ 71 | module.exports.fetchById = function(clientId, cb) { 72 | /** 73 | * For example: 74 | * 75 | */ 76 | throw new error.serverError('Client model method "fetchById" is not implemented'); 77 | }; 78 | 79 | /** 80 | * Checks secret for the client 81 | * Function arguments MAY be different 82 | * 83 | * @param client {Object} Client object 84 | * @param secret {String} Password to be checked 85 | */ 86 | module.exports.checkSecret = function(client, secret, cb) { 87 | /** 88 | * For example: 89 | * superHashFunction(secret, function(err, hash){ 90 | * if(err){ 91 | * return cb(err); 92 | * } 93 | * 94 | * cb(null,client.secret === hash) 95 | * }); 96 | * 97 | * OR for sync hash function 98 | * 99 | * cb(null, client.secret != superHashFunction(secret)); 100 | * 101 | */ 102 | throw new error.serverError('Client model method "checkSecret" is not implemented'); 103 | }; 104 | 105 | /** 106 | * Checks grant type permission for the client 107 | * Default: do not check it 108 | * Function arguments MAY be different 109 | * 110 | * @param client {Object} Client object 111 | * @param grant {String} Grant type to be checked for 112 | */ 113 | module.exports.checkGrantType = function(client, grant) { 114 | /** 115 | * For example: 116 | * if (client.grants.indexOf(grant) !== -1) return true; 117 | * else false; 118 | */ 119 | return true; 120 | }; 121 | 122 | /** 123 | * Checks scope permission for the client 124 | * Default: do not check it, return empty array 125 | * Function arguments MAY be different 126 | * 127 | * @param client {Object} Client object 128 | * @param scope {String} Scope string (space delimited) passed via parameters 129 | * @return {Array|boolean} Return checked scope array or false 130 | */ 131 | module.exports.checkScope = function(client, scope) { 132 | /** 133 | * For example: 134 | * scope.forEach(function(item) { 135 | * if (scope.indexOf(item) == -1) return false; 136 | * }); 137 | * return scope; 138 | */ 139 | return []; 140 | }; 141 | 142 | /** 143 | * Transforms scope body parameter to scope array 144 | * 145 | * @param scope 146 | */ 147 | module.exports.transformScope = function(scope) { 148 | if (!scope) return []; 149 | return scope.split(' '); 150 | }; -------------------------------------------------------------------------------- /lib/controller/token/password.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | error = require('./../../error'); 4 | 5 | module.exports = function(oauth2, client, username, password, scope, pCb) { 6 | 7 | // Define variables 8 | var user; 9 | var responseObj = { 10 | token_type: "bearer" 11 | }; 12 | 13 | async.waterfall([ 14 | // Check username and password parameters 15 | function(cb) { 16 | if (!username) 17 | return cb(new error.invalidRequest('Username is mandatory for password grant type')); 18 | oauth2.logger.debug('Username parameter check passed: ', username); 19 | 20 | if (!password) 21 | return cb(new error.invalidRequest('Password is mandatory for password grant type')); 22 | oauth2.logger.debug('Password parameter check passed: ', password); 23 | 24 | cb(); 25 | }, 26 | // Parse and check scope against supported and client available scopes 27 | function(cb) { 28 | scope = oauth2.model.client.transformScope(scope); 29 | scope = oauth2.model.client.checkScope(client, scope); 30 | if (!scope) 31 | cb(new error.invalidScope('Invalid scope for the client')); 32 | else { 33 | oauth2.logger.debug('Scope check passed: ', scope); 34 | cb(); 35 | } 36 | }, 37 | // Fetch user 38 | function(cb) { 39 | oauth2.model.user.fetchByUsername(username, function(err, obj) { 40 | if (err) 41 | cb(new error.serverError('Failed to call user::fetchByUsername method')); 42 | else if (!obj) 43 | cb(new error.invalidClient('User not found')); 44 | else { 45 | oauth2.logger.debug('User fetched: ', obj); 46 | user = obj; 47 | cb(); 48 | } 49 | }); 50 | }, 51 | // Check provided password 52 | function(cb) { 53 | oauth2.model.user.checkPassword(user, password, function(err, valid) { 54 | if (err) 55 | cb(new error.serverError('Failed to call user:checkPassword method')); 56 | else if (!valid) 57 | cb(new error.invalidClient('Wrong user password provided')); 58 | else 59 | cb(); 60 | }); 61 | }, 62 | // Remove old refreshToken (if exists) with userId-clientId pair 63 | function(cb) { 64 | oauth2.model.refreshToken.removeByUserIdClientId(oauth2.model.user.getId(user), oauth2.model.client.getId(client), function(err) { 65 | if (err) 66 | cb(new error.serverError('Failed to call refreshToken::removeByUserIdClientId method')); 67 | else { 68 | oauth2.logger.debug('Refresh token removed'); 69 | cb(); 70 | } 71 | }); 72 | }, 73 | // Generate new refreshToken and save it 74 | function(cb) { 75 | //check if client has grant type refresh_token, if not, it will not be including in response (short time authorization) 76 | if(!oauth2.model.client.checkGrantType(client, 'refresh_token')){ 77 | oauth2.logger.debug('Client has not the grant type refresh_token, skip creation'); 78 | return cb(); 79 | } 80 | 81 | oauth2.model.refreshToken.create(oauth2.model.user.getId(user), oauth2.model.client.getId(client), scope, function(err, data) { 82 | if (err) 83 | cb(new error.serverError('Failed to call refreshToken::save method')); 84 | else { 85 | responseObj.refresh_token = data; 86 | oauth2.logger.debug('Refresh token saved: ', responseObj.refresh_token); 87 | cb(); 88 | } 89 | }); 90 | }, 91 | // Generate new accessToken and save it 92 | function(cb) { 93 | oauth2.model.accessToken.create(oauth2.model.user.getId(user), oauth2.model.client.getId(client), scope, oauth2.model.accessToken.ttl, function(err, data) { 94 | if (err) 95 | cb(new error.serverError('Failed to call accessToken::save method')); 96 | else { 97 | responseObj.access_token = data; 98 | responseObj.expires_in = oauth2.model.accessToken.ttl; 99 | oauth2.logger.debug('Access token saved: ', responseObj.access_token); 100 | cb(); 101 | } 102 | }); 103 | } 104 | ], 105 | function(err) { 106 | if (err) pCb(err); 107 | else { 108 | pCb(null, { event: 'token_granted_from_password', data:responseObj}); 109 | } 110 | }); 111 | }; -------------------------------------------------------------------------------- /lib/controller/token.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | token = require('./token/'), 4 | response = require('./../util/response.js'), 5 | error = require('./../error'), 6 | emitter = require('./../events'); 7 | 8 | /** 9 | * Token endpoint controller 10 | * Used for: "authorization_code", "password", "client_credentials", "refresh_token" flows 11 | * 12 | * @see http://tools.ietf.org/html/rfc6749#section-3.2 13 | * @param req Request object 14 | * @param res Response object 15 | */ 16 | module.exports = function(req, res) { 17 | req.oauth2.logger.debug('Invoking token endpoint'); 18 | 19 | var clientId, 20 | clientSecret, 21 | grantType, 22 | client; 23 | 24 | async.waterfall([ 25 | // Parse client credentials from request body 26 | // if credentials are not exist in request body, 27 | // then check client credentials from BasicAuth header 28 | function(cb) { 29 | if (req.body.client_id && req.body.client_secret) { 30 | clientId = req.body.client_id; 31 | clientSecret = req.body.client_secret; 32 | req.oauth2.logger.debug('Client credentials parsed from body parameters: ', clientId, clientSecret); 33 | cb(); 34 | } else { 35 | if (!req.headers || !req.headers.authorization) 36 | return cb(new error.invalidRequest('No authorization header passed')); 37 | 38 | var pieces = req.headers.authorization.split(' ', 2); 39 | if (!pieces || pieces.length !== 2) 40 | return cb(new error.invalidRequest('Authorization header is corrupted')); 41 | 42 | if (pieces[0] !== 'Basic') 43 | return cb(new error.invalidRequest('Unsupported authorization method: ', pieces[0])); 44 | 45 | pieces = new Buffer(pieces[1], 'base64').toString('ascii').split(':', 2); 46 | if (!pieces || pieces.length !== 2) 47 | return cb(new error.invalidRequest('Authorization header has corrupted data')); 48 | 49 | clientId = pieces[0]; 50 | clientSecret = pieces[1]; 51 | req.oauth2.logger.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret); 52 | cb(); 53 | } 54 | }, 55 | // Check grant type for server support 56 | function(cb) { 57 | if (!req.body.grant_type) 58 | cb(new error.invalidRequest('Body does not contain grant_type parameter')); 59 | 60 | grantType = req.body.grant_type; 61 | req.oauth2.logger.debug('Parameter grant_type passed: ', grantType); 62 | cb(); 63 | }, 64 | // Fetch client and check credentials 65 | function(cb) { 66 | req.oauth2.model.client.fetchById(clientId, function(err, obj) { 67 | if (err) 68 | cb(new error.serverError('Failed to call client::fetchById method')); 69 | else if (!obj) 70 | cb(new error.invalidClient('Client not found')); 71 | else { 72 | req.oauth2.logger.debug('Client fetched: ', obj); 73 | client = obj; 74 | cb(); 75 | } 76 | }); 77 | }, 78 | function(cb){ 79 | req.oauth2.model.client.checkSecret(client, clientSecret, function(err, valid){ 80 | if(err) 81 | cb(new error.serverError('Failed to call client::checkSecret method')); 82 | else if (!valid) 83 | cb(new error.invalidClient('Wrong client secret provided')); 84 | else 85 | cb(); 86 | }); 87 | }, 88 | // Check grant type against client available 89 | function(cb) { 90 | if (!req.oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'refresh_token') 91 | cb(new error.unauthorizedClient('Grant type is not available for the client')); 92 | else { 93 | req.oauth2.logger.debug('Grant type check passed'); 94 | cb(); 95 | } 96 | }, 97 | function(cb) { 98 | switch (grantType) { 99 | case 'authorization_code': 100 | token.authorizationCode(req.oauth2, client, req.body.code, req.body.redirect_uri, cb); 101 | break; 102 | case 'password': 103 | token.password(req.oauth2, client, req.body.username, req.body.password, req.body.scope, cb); 104 | break; 105 | case 'client_credentials': 106 | token.clientCredentials(req.oauth2, client, req.body.scope, cb); 107 | break; 108 | case 'refresh_token': 109 | token.refreshToken(req.oauth2, client, req.body.refresh_token, req.body.scope, cb); 110 | break; 111 | default: 112 | cb(new error.unsupportedGrantType('Grant type does not match any supported type')); 113 | break; 114 | } 115 | } 116 | ], 117 | function(err, data) { 118 | if (err) response.error(req, res, err); 119 | else { 120 | emitter.token_granted(data.event, req, data.data); 121 | response.data(req, res, data.data); 122 | } 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /lib/controller/authorization.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | authorization = require('./authorization/'), 4 | response = require('./../util/response.js'), 5 | error = require('./../error'); 6 | 7 | /** 8 | * Authorization Endpoint controller 9 | * Used for: "authorization_code", "implicit" flows 10 | * 11 | * @see http://tools.ietf.org/html/rfc6749#section-3.1 12 | * @param req Request object 13 | * @param res Response object 14 | * @param next Optional parameter 15 | */ 16 | module.exports = function(req, res, next) { 17 | req.oauth2.logger.debug('Invoking authorization endpoint'); 18 | 19 | var clientId, 20 | redirectUri, 21 | responseType, 22 | grantType, 23 | client, 24 | scope, 25 | user; 26 | 27 | async.waterfall([ 28 | // Check redirect uri 29 | function(cb) { 30 | if (!req.query.redirect_uri) 31 | return cb(new error.invalidRequest('RedirectUri is mandatory for authorization endpoint')); 32 | 33 | redirectUri = req.query.redirect_uri; 34 | req.oauth2.logger.debug('RedirectUri parsed: ', redirectUri); 35 | cb(); 36 | }, 37 | // Check client credentials 38 | function(cb) { 39 | if (!req.query.client_id) 40 | return cb(new error.invalidRequest('ClientId is mandatory for authorization endpoint')); 41 | 42 | // Check for client_secret (prevent from passing it) 43 | if (req.query.client_secret) 44 | return cb(new error.invalidRequest('ClientSecret should not be passed by public clients')); 45 | 46 | clientId = req.query.client_id; 47 | req.oauth2.logger.debug('ClientId parsed: ', clientId); 48 | cb(); 49 | }, 50 | // Check response type parameter 51 | function(cb) { 52 | if (!req.query.response_type) 53 | return cb(new error.invalidRequest('ResponseType parameter is mandatory for authorization endpoint')); 54 | 55 | responseType = req.query.response_type; 56 | req.oauth2.logger.debug('Parameter response_type parsed: ', responseType); 57 | cb(); 58 | }, 59 | // Check grant type supported by server 60 | function(cb) { 61 | switch (responseType) { 62 | case 'code': 63 | grantType = 'authorization_code'; 64 | break; 65 | case 'token': 66 | grantType = 'implicit'; 67 | break; 68 | default: 69 | return cb(new error.unsupportedResponseType('Unknown response_type parameter passed')); 70 | break; 71 | } 72 | req.oauth2.logger.debug('Parameter response_type parsed: ', responseType); 73 | cb(); 74 | }, 75 | // Fetch client 76 | function(cb) { 77 | req.oauth2.model.client.fetchById(clientId, function(err, obj) { 78 | if (err) 79 | cb(new error.serverError('Failed to call client::fetchById method')); 80 | else if (!obj) 81 | cb(new error.invalidClient('Client not found')); 82 | else { 83 | req.oauth2.logger.debug('Client fetched: ', obj); 84 | client = obj; 85 | cb(); 86 | } 87 | }); 88 | }, 89 | // Check redirect uri 90 | function(cb) { 91 | if (!req.oauth2.model.client.getRedirectUri(client)) 92 | cb(new error.unsupportedResponseType('RedirectUri is not set for the client')); 93 | else if (!req.oauth2.model.client.checkRedirectUri(client, redirectUri)) 94 | cb(new error.invalidRequest('Wrong RedirectUri provided')); 95 | else { 96 | req.oauth2.logger.debug('RedirectUri check passed: ', redirectUri); 97 | cb(); 98 | } 99 | }, 100 | // Check grant type available for the client 101 | function(cb) { 102 | if (!req.oauth2.model.client.checkGrantType(client, grantType)) 103 | cb(new error.unauthorizedClient('Grant type is not available for the client')); 104 | else { 105 | req.oauth2.logger.debug('Grant type check passed'); 106 | cb(); 107 | } 108 | }, 109 | // Parse and check scope against supported and client available scopes 110 | function(cb) { 111 | scope = req.oauth2.model.client.transformScope(req.query.scope); 112 | scope = req.oauth2.model.client.checkScope(client, scope); 113 | if (!scope) 114 | cb(new error.invalidScope('Invalid scope for the client')); 115 | else { 116 | req.oauth2.logger.debug('Scope check passed: ', scope); 117 | cb(); 118 | } 119 | }, 120 | // Fetch user from request 121 | function(cb) { 122 | user = req.oauth2.model.user.fetchFromRequest(req); 123 | if (!user) 124 | cb(new error.invalidRequest('Failed to fetch logged user from request parameters')); 125 | else { 126 | req.oauth2.logger.debug('User fetched from request: ', user); 127 | cb(); 128 | } 129 | } 130 | ], 131 | function(err) { 132 | if (err) response.error(req, res, err, redirectUri); 133 | else { 134 | if (req.method == 'GET') 135 | req.oauth2.decision(req, res, client, scope, user, redirectUri); 136 | else if (grantType == 'authorization_code') 137 | authorization.code(req, res, client, scope, user, redirectUri); 138 | else if (grantType == 'implicit') 139 | authorization.implicit(req, res, client, scope, user, redirectUri); 140 | else 141 | response.error(req, res, new error.invalidRequest('Wrong request method'), redirectUri); 142 | } 143 | }); 144 | }; -------------------------------------------------------------------------------- /lib/controller/token/refreshToken.js: -------------------------------------------------------------------------------- 1 | var 2 | async = require('async'), 3 | error = require('./../../error'); 4 | 5 | module.exports = function(oauth2, client, refresh_token, scope, pCb) { 6 | // Define variables 7 | var user, 8 | ttl, 9 | refreshToken, 10 | accessToken; 11 | 12 | var responseObj = { 13 | // @todo: add renew refresh token strategy 14 | token_type: "bearer" 15 | }; 16 | 17 | async.waterfall([ 18 | // Check refresh_token parameter 19 | function(cb) { 20 | if (!refresh_token) 21 | return cb(new error.invalidRequest('RefreshToken is mandatory for refresh_token grant type')); 22 | oauth2.logger.debug('RefreshToken parameter check passed: ', refresh_token); 23 | 24 | cb(); 25 | }, 26 | // Standard is really weird here, do not check scope, just fill it from refreshToken 27 | // function(cb) {CHECK SCOPE PARAMETER FUNCTION OMITTED}, 28 | // Fetch refreshToken 29 | function(cb) { 30 | oauth2.model.refreshToken.fetchByToken(refresh_token, function(err, obj) { 31 | if (err) 32 | cb(new error.serverError('Failed to call refreshToken::fetchByToken method')); 33 | else if (!obj) 34 | cb(new error.invalidGrant('Refresh token not found')); 35 | else if (oauth2.model.refreshToken.getClientId(obj) != oauth2.model.client.getId(client)) { 36 | oauth2.logger.warn('Client id "' + oauth2.model.client.getId(client) + '" tried to fetch client id "' + oauth2.model.refreshToken.getClientId(obj) + '" refresh token'); 37 | cb(new error.invalidGrant('Refresh token not found')); 38 | } 39 | else { 40 | oauth2.logger.debug('RefreshToken fetched: ', obj); 41 | refreshToken = obj; 42 | cb(); 43 | } 44 | }); 45 | }, 46 | // Fetch user 47 | function(cb) { 48 | oauth2.model.user.fetchById(oauth2.model.refreshToken.getUserId(refreshToken), function(err, obj) { 49 | if (err) 50 | cb(new error.serverError('Failed to call user::fetchById method')); 51 | else if (!obj) 52 | cb(new error.invalidClient('User not found')); 53 | else { 54 | oauth2.logger.debug('User fetched: ', obj); 55 | user = obj; 56 | cb(); 57 | } 58 | }); 59 | }, 60 | // Fetch issued access token (if it is already created and still active) 61 | function(cb) { 62 | oauth2.model.accessToken.fetchByUserIdClientId(oauth2.model.user.getId(user), oauth2.model.client.getId(client), function(err, obj) { 63 | if (err) 64 | cb(new error.serverError('Failed to call accessToken::fetchByUserIdClientId')); 65 | else if (!obj) cb(); 66 | else { 67 | accessToken = obj; 68 | oauth2.logger.debug('Fetched issued accessToken: ', obj); 69 | cb(); 70 | } 71 | }); 72 | }, 73 | // Issue new one (if needed) 74 | function(cb) { 75 | // No need if it already exists and valid 76 | if (accessToken) { 77 | return oauth2.model.accessToken.getTTL(accessToken, function(err, ttl){ 78 | if(err){ 79 | return cb(new error.serverError('Failed to call accessToken::getTTL')); 80 | } 81 | 82 | if(!ttl) { 83 | accessToken = null; 84 | } 85 | else { 86 | responseObj.access_token = oauth2.model.accessToken.getToken(accessToken); 87 | responseObj.expires_in = ttl; 88 | } 89 | 90 | cb(); 91 | }); 92 | } 93 | 94 | cb(); 95 | }, 96 | function(cb){ 97 | if(!accessToken){ 98 | return oauth2.model.accessToken.create(oauth2.model.user.getId(user), oauth2.model.client.getId(client), oauth2.model.refreshToken.getScope(refreshToken), oauth2.model.accessToken.ttl, function(err, data) { 99 | if (err) 100 | cb(new error.serverError('Failed to call accessToken::save method')); 101 | else { 102 | responseObj.access_token = data; 103 | responseObj.expires_in = oauth2.model.accessToken.ttl; 104 | oauth2.logger.debug('Access token saved: ', responseObj.access_token); 105 | 106 | // Issue new refresh token when refreshing access token 107 | if (oauth2.renewRefreshToken) { 108 | oauth2.model.refreshToken.create(oauth2.model.user.getId(user), oauth2.model.client.getId(client), oauth2.model.refreshToken.getScope(refreshToken), function(err, data) { 109 | if (err) 110 | cb(new error.serverError('Failed to call refreshToken::save method')); 111 | else { 112 | oauth2.model.refreshToken.removeByRefreshToken(refresh_token, function(err) { 113 | if (err) 114 | cb(new error.serverError('Failed to call refreshToken::removeByRefreshToken method')); 115 | else { 116 | oauth2.logger.debug('Refresh token removed'); 117 | } 118 | }); 119 | responseObj.refresh_token = data; 120 | oauth2.logger.debug('Refresh token saved: ', responseObj.refresh_token); 121 | cb(); 122 | } 123 | }); 124 | } else { 125 | cb(); 126 | } 127 | } 128 | }); 129 | } 130 | 131 | cb(); 132 | } 133 | ], 134 | function(err) { 135 | if (err) pCb(err); 136 | else { 137 | pCb(null, { event: 'token_granted_from_refresh_token', data:responseObj}); 138 | } 139 | }); 140 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-oauth20-provider 2 | ================== 3 | 4 | OAuth 2.0 provider toolkit for nodeJS with connect/express support. Supports all the four authorization flows: authorization code, implicit, client credentials, password. 5 | 6 | ___One more oAuth 2.0 service provider? Yes!___ 7 | 8 | - Based on the final specification *[RFC6749](http://tools.ietf.org/html/rfc6749 "The OAuth 2.0 Authorization Framework Specification")* 9 | - Fully customizable and extremely flexible 10 | - Test covered 11 | - Contains integration examples 12 | 13 | ## Installation ## 14 | 15 | $ npm install oauth20-provider 16 | 17 | --------------------------------------- 18 | 19 | ## Usage ## 20 | 21 | > Look into ."/test/server/ directory" for working example with in-memory and redis storage models. 22 | 23 | ### Step 1. Define your models and decision controller ### 24 | 25 | By default module sets abstract models which should be redefined, library does not force any implementation. 26 | 27 | ### Refresh token model ### 28 | 29 | Refresh tokens are credentials used to obtain access tokens. Refresh tokens are issued to the client by the authorization server and are used to obtain a new access token when the current access token becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope (access tokens may have a shorter lifetime and fewer permissions than authorized by the resource owner). *[Read more](http://tools.ietf.org/html/rfc6749#section-1.5)* 30 | 31 | ___Redefinable functions___ 32 | Look at ["./lib/model/refreshToken.js"](./lib/model/refreshToken.js) for further information. 33 | 34 | 35 | ### Access token model ### 36 | 37 | Access tokens are credentials used to access protected resources. An access token is a string representing an authorization issued to the client. The string is usually opaque to the client. Tokens represent specific scopes and durations of access, granted by the resource owner, and enforced by the resource server and authorization server. *[Read more](http://tools.ietf.org/html/rfc6749#section-1.4)* 38 | 39 | ___Redefinable functions___ 40 | Look at ["./lib/model/accessToken.js"](./lib/model/accessToken.js) for further information. 41 | 42 | ### Client model ### 43 | 44 | Before initiating the protocol, the client registers with the authorization server. The means through which the client registers with the authorization server are beyond the scope of this specification but typically involve end-user interaction with an HTML registration form. *[Read more](http://tools.ietf.org/html/rfc6749#section-2)* 45 | 46 | ___Redefinable functions___ 47 | Look at ["./lib/model/client.js"](./lib/model/client.js) for further information. 48 | 49 | ### Code model ### 50 | 51 | The authorization code is obtained by using an authorization server as an intermediary between the client and resource owner. User only by authorization code flow, no need to initialize it if one don't use this grant. *[Read more](http://tools.ietf.org/html/rfc6749#section-1.3.1)* 52 | 53 | ___Redefinable functions___ 54 | Look at ["./lib/model/code.js"](./lib/model/code.js) for further information. 55 | 56 | ### User model ### 57 | 58 | User is a registered person in the service. Model should contain unique identifier, password and sometimes additional unique key (for example username/email). There is no common scheme for this type of object, feel free to implement it your way. 59 | 60 | ___Redefinable functions___ 61 | Look at ["./lib/model/user.js"](./lib/model/user.js) for further information. 62 | 63 | ### Decision controller ### 64 | 65 | Page is used to ask user whether user agree or not to allow client to access his information with current scope. Controller should return POST form with decision parameter (0 - user does not allow, 1 - user allows). 66 | 67 | ___Redefinable___ 68 | Look at ["./lib/controller/authorization/decision.js"](./lib/controller/authorization/decision.js) for further information. 69 | 70 | ### Step 2. Inject and Define Endpoints ### 71 | 72 | First of all include and initialize **oauth20-provider** library: 73 | ```js 74 | var oauth2lib = require('oauth20-provider'); 75 | var oauth2 = new oauth2lib({log: {level: 2}}); 76 | ``` 77 | 78 | Library is compatible with express/connect servers, inject oauth2 into your server. 79 | ```js 80 | server.use(oauth2.inject()); 81 | ``` 82 | 83 | ___Token endpoint___ 84 | 85 | ```js 86 | server.post('/token', oauth2.controller.token); 87 | ``` 88 | 89 | ___Authorization endpoint___ 90 | 91 | ```js 92 | server.get('/authorization', isAuthorized, oauth2.controller.authorization, function(req, res) { 93 | // Render our decision page 94 | // Look into ./test/server for further information 95 | res.render('authorization', {layout: false}); 96 | }); 97 | server.post('/authorization', isAuthorized, oauth2.controller.authorization); 98 | ``` 99 | 100 | Middleware **isAuthorized** is used to check user login. If user is not logged in - show authorization form instead. Simple implementation: 101 | 102 | ```js 103 | function isAuthorized(req, res, next) { 104 | if (req.session.authorized) next(); 105 | else { 106 | var params = req.query; 107 | params.backUrl = req.path; 108 | res.redirect('/login?' + query.stringify(params)); 109 | } 110 | }; 111 | ``` 112 | 113 | ### Step 3. Relax ### 114 | 115 | Your authorization server is ready for work. 116 | 117 | --------------------------------------- 118 | 119 | ### ToDo list and future plans ### 120 | 121 | - Add examples (mongodb, postgresql) 122 | - Allow multiple flows for single client (only 1 per client works well yet) 123 | - Add refresh token TTL 124 | - Implement proper "TTL" support for accessToken and scope objects 125 | - Add MAC token type 126 | - Allow client authentication via query parameters 127 | - More tests 128 | - More docs 129 | - Build example site 130 | - Check RFC once more 131 | 132 | ### License ### 133 | 134 | Copyright (c) 2013 Tim Shamilov 135 | 136 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 139 | 140 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 141 | --------------------------------------------------------------------------------