├── 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 | '
', 63 | '' 67 | ]; 68 | res.send(html.join('