diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..aa10c91 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,92 @@ +{ + "requireCurlyBraces": [ + "while", + "do", + "try", + "catch" + ], + "requireSpaceAfterKeywords": [ + "else", + "while", + "do", + "return", + "try" + ], + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "requireSpaceBeforeBlockStatements": true, + "requireSpacesInForStatement": true, + "disallowKeywords": [ + "with" + ], + "requireLineFeedAtFileEnd": true, + "validateLineBreaks": "LF", + "requireSpaceAfterPrefixUnaryOperators": [ + "++", + "--" + ], + "requireSpaceBetweenArguments": true, + "requireSpaceBeforePostfixUnaryOperators": [ + "--" + ], + "requireSpaceBeforeBinaryOperators": [ + "-", + "=", + "==", + "===", + "!==", + ">", + ">=", + "<=" + ], + "requireSpaceAfterBinaryOperators": [ + "-", + "=", + "==", + "===", + "!==", + ">", + ">=", + "<=" + ], + "disallowSpaceBeforeBinaryOperators": [ + "+", + "+", + "/", + "*", + "!=", + "<", + "<" + ], + "disallowPaddingNewlinesInBlocks": true, + "validateIndentation": 2, + "disallowSpaceAfterKeywords": [ + "if", + "if", + "for", + "for", + "switch", + "switch", + "catch", + "catch" + ], + "disallowSpacesInConditionalExpression": { + "beforeAlternate": true + }, + "disallowSpaceAfterBinaryOperators": [ + "+", + "+", + "/", + "*", + "!=", + "<", + "<" + ], + "disallowSpacesInsideObjectBrackets": "all", + "disallowSpacesInsideArrayBrackets": "all", + "disallowSpaceBeforePostfixUnaryOperators": [ + "++", + "++" + ] +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..442455e --- /dev/null +++ b/.jshintrc @@ -0,0 +1,18 @@ +{ + "node": true, + "bitwise": true, + "camelcase": false, + "curly": false, + "forin": true, + "immed": true, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "nonew": true, + "quotmark": "single", + "undef": true, + "unused": true, + "trailing": true, + "laxcomma": true +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..b085a47 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,40 @@ +# Steps To Run Example + +You must have [Node.js](http://nodejs.org/) and [Redis](http://redis.io/download) installed, and Redis must be running. + +1. Fork this repository +2. Clone it, using your username + + ``` + git clone git@github.com:/OpenIDConnect.git + cd OpenIDConnect + ``` + +3. Install Node.js dependencies + + ``` + npm install + cd examples + npm install + ``` + +4. Start server + + ``` + node openid-connect-example.js + ``` + +5. Create user: http://localhost:3001/user/create +6. Register a client: http://localhost:3001/client/register + * Client Key: exampleid + * Redirect URI: http://localhost:3001/test +7. Test an auth flow at http://localhost:3001/test + * Client Key: exampleid + * Scopes: foo + * Follow prompts + - Accept + - Get Token + - Get Resource + - You should see page that is restricted by ```foo``` scope +8. Logout: http://localhost:3001/logout?access_token=YOUR_ACCESS_TOKEN +9. Navigate to http://localhost:3001/user/foo?access_token=YOUR_ACCESS_TOKEN diff --git a/examples/openid-connect-example.js b/examples/openid-connect-example.js index c752b0b..6d9838b 100644 --- a/examples/openid-connect-example.js +++ b/examples/openid-connect-example.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -7,7 +6,7 @@ var crypto = require('crypto'), express = require('express'), expressSession = require('express-session'), http = require('http'), - path = require('path'), + //path = require('path'), querystring = require('querystring'), rs = require('connect-redis')(expressSession), extend = require('extend'), @@ -29,20 +28,29 @@ var options = { foo: 'Access to foo special resource', bar: 'Access to bar special resource' }, -//when this line is enabled, user email appears in tokens sub field. By default, id is used as sub. - models:{user:{attributes:{sub:function(){return this.email;}}}}, + //when this line is enabled, user email appears in tokens sub field. By default, id is used as sub. + models:{user:{attributes:{sub:function() {return this.email;}}}}, app: app }; var oidc = require('../index').oidc(options); - // all environments app.set('port', process.env.PORT || 3001); app.use(logger('dev')); -app.use(bodyParser()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ + extended: true +})); app.use(methodOverride()); app.use(cookieParser('Some Secret!!!')); -app.use(expressSession({store: new rs({host: '127.0.0.1', port: 6379}), secret: 'Some Secret!!!'})); +//* +app.use(expressSession({ + store: new rs({host: '127.0.0.1', port: 6379}), + secret: 'Some Secret!!!', + saveUninitialized: true, + resave: true +})); +//*/ // app.use(app.router); //redirect to login @@ -52,6 +60,7 @@ app.get('/', function(req, res) { //Login form (I use email as user name) app.get('/my/login', function(req, res, next) { + /*jshint unused:false */ var head = 'Login'; var inputs = ''; var error = req.session.error?'
'+req.session.error+'
':''; @@ -62,30 +71,33 @@ app.get('/my/login', function(req, res, next) { var validateUser = function (req, next) { delete req.session.error; req.model.user.findOne({email: req.body.email}, function(err, user) { - if(!err && user && user.samePassword(req.body.password)) { - return next(null, user); - } else { - var error = new Error('Username or password incorrect.'); - return next(error); - } + if(!err && user && user.samePassword(req.body.password)) { + return next(null, user); + } else { + var error = new Error('Username or password incorrect.'); + return next(error); + } }); }; var afterLogin = function (req, res, next) { - res.redirect(req.param('return_url')||'/user'); + /*jshint unused:false */ + res.redirect(req.param('return_url')||'/user'); }; var loginError = function (err, req, res, next) { - req.session.error = err.message; - res.redirect(req.path); + /*jshint unused:false */ + req.session.error = err.message; + res.redirect(req.path); }; app.post('/my/login', oidc.login(validateUser), afterLogin, loginError); app.all('/logout', oidc.removetokens(), function(req, res, next) { - req.session.destroy(); - res.redirect('/my/login'); + /*jshint unused:false */ + req.session.destroy(); + res.redirect('/my/login'); }); //authorization endpoint @@ -96,10 +108,13 @@ app.post('/user/token', oidc.token()); //user consent form app.get('/user/consent', function(req, res, next) { + /*jshint unused:false */ var head = 'Consent'; var lis = []; for(var i in req.session.scopes) { - lis.push('
  • '+i+': '+req.session.scopes[i].explain+'
  • '); + if(req.session.scopes.hasOwnProperty(i)) { + lis.push('
  • '+i+': '+req.session.scopes[i].explain+'
  • '); + } } var ul = ''; var error = req.session.error?'
    '+req.session.error+'
    ':''; @@ -112,37 +127,39 @@ app.post('/user/consent', oidc.consent()); //user creation form app.get('/user/create', function(req, res, next) { + /*jshint unused:false */ var head = 'Sign in'; var inputs = ''; - //var fields = mkFields(oidc.model('user').attributes); var fields = { - given_name: { - label: 'Given Name', - type: 'text' - }, - middle_name: { - label: 'Middle Name', - type: 'text' - }, - family_name: { - label: 'Family Name', - type: 'text' - }, - email: { - label: 'Email', - type: 'email' - }, - password: { - label: 'Password', - type: 'password' - }, - passConfirm: { - label: 'Confirm Password', - type: 'password' - } + given_name: { + label: 'Given Name', + type: 'text' + }, + middle_name: { + label: 'Middle Name', + type: 'text' + }, + family_name: { + label: 'Family Name', + type: 'text' + }, + email: { + label: 'Email', + type: 'email' + }, + password: { + label: 'Password', + type: 'password' + }, + passConfirm: { + label: 'Confirm Password', + type: 'password' + } }; for(var i in fields) { - inputs += '
    '; + if(fields.hasOwnProperty(i)) { + inputs += '
    '; + } } var error = req.session.error?'
    '+req.session.error+'
    ':''; var body = '

    Sign in

    '+inputs+'
    '+error; @@ -151,94 +168,101 @@ app.get('/user/create', function(req, res, next) { //process user creation app.post('/user/create', oidc.use({policies: {loggedIn: false}, models: 'user'}), function(req, res, next) { + /*jshint unused:false */ delete req.session.error; req.model.user.findOne({email: req.body.email}, function(err, user) { - if(err) { - req.session.error=err; - } else if(user) { - req.session.error='User already exists.'; - } - if(req.session.error) { + if(err) { + req.session.error = err; + } else if(user) { + req.session.error = 'User already exists.'; + } + if(req.session.error) { + res.redirect(req.path); + } else { + req.body.name = req.body.given_name+' '+(req.body.middle_name?req.body.middle_name+' ':'')+req.body.family_name; + req.model.user.create(req.body, function(err, user) { + if(err || !user) { + req.session.error = err?err:'User could not be created.'; res.redirect(req.path); - } else { - req.body.name = req.body.given_name+' '+(req.body.middle_name?req.body.middle_name+' ':'')+req.body.family_name; - req.model.user.create(req.body, function(err, user) { - if(err || !user) { - req.session.error=err?err:'User could not be created.'; - res.redirect(req.path); - } else { - req.session.user = user.id; - res.redirect('/user'); - } - }); - } + } else { + req.session.user = user.id; + res.redirect('/user'); + } + }); + } }); }); -app.get('/user', oidc.check(), function(req, res, next){ +app.get('/user', oidc.check(), function(req, res, next) { + /*jshint unused:false */ res.send('

    User Page

    See registered clients of user
    '); }); //User Info Endpoint app.get('/api/user', oidc.userInfo()); -app.get('/user/foo', oidc.check('foo'), function(req, res, next){ - res.send('

    Page Restricted by foo scope

    '); +app.get('/user/foo', oidc.check('foo'), function(req, res, next) { + /*jshint unused:false */ + res.send('

    Success!

    You are now viewing a page restricted by the "foo" scope.

    '); }); -app.get('/user/bar', oidc.check('bar'), function(req, res, next){ - res.send('

    Page restricted by bar scope

    '); +app.get('/user/bar', oidc.check('bar'), function(req, res, next) { + /*jshint unused:false */ + res.send('

    Success!

    You are now viewing a page restricted by the "bar" scope.

    '); }); -app.get('/user/and', oidc.check('bar', 'foo'), function(req, res, next){ - res.send('

    Page restricted by "bar and foo" scopes

    '); +app.get('/user/and', oidc.check('bar', 'foo'), function(req, res, next) { + /*jshint unused:false */ + res.send('

    Success!

    You are now viewing a page restricted by the "bar and foo" scopes.

    '); }); -app.get('/user/or', oidc.check(/bar|foo/), function(req, res, next){ - res.send('

    Page restricted by "bar or foo" scopes

    '); +app.get('/user/or', oidc.check(/bar|foo/), function(req, res, next) { + /*jshint unused:false */ + res.send('

    Success!

    You are now viewing a page restricted by the "bar or foo" scopes.

    '); }); //Client register form app.get('/client/register', oidc.use('client'), function(req, res, next) { - var mkId = function() { var key = crypto.createHash('md5').update(req.session.user+'-'+Math.random()).digest('hex'); req.model.client.findOne({key: key}, function(err, client) { if(!err && !client) { - var secret = crypto.createHash('md5').update(key+req.session.user+Math.random()).digest('hex'); - req.session.register_client = {}; - req.session.register_client.key = key; - req.session.register_client.secret = secret; - var head = 'Register Client'; - var inputs = ''; - var fields = { - name: { - label: 'Client Name', - html: '' - }, - redirect_uris: { - label: 'Redirect Uri', - html: '' - }, - key: { - label: 'Client Key', - html: ''+key+'' - }, - secret: { - label: 'Client Secret', - html: ''+secret+'' - } - }; - for(var i in fields) { + var secret = crypto.createHash('md5').update(key+req.session.user+Math.random()).digest('hex'); + req.session.register_client = {}; + req.session.register_client.key = key; + req.session.register_client.secret = secret; + var head = 'Register Client'; + var inputs = ''; + var fields = { + name: { + label: 'Client Name', + html: '' + }, + redirect_uris: { + label: 'Redirect Uri', + html: '' + }, + key: { + label: 'Client Key', + html: ''+key+'' + }, + secret: { + label: 'Client Secret', + html: ''+secret+'' + } + }; + for(var i in fields) { + if(fields.hasOwnProperty(i)) { inputs += '
    '+fields[i].html+'
    '; } - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Register Client

    '+inputs+'
    '+error; - res.send(''+head+body+''); + } + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Register Client

    '+inputs+'
    '+error; + res.send(''+head+body+''); } else if(!err) { - mkId(); + mkId(); } else { - next(err); + next(err); } }); }; @@ -247,12 +271,12 @@ app.get('/client/register', oidc.use('client'), function(req, res, next) { //process client register app.post('/client/register', oidc.use('client'), function(req, res, next) { - delete req.session.error; + delete req.session.error; req.body.key = req.session.register_client.key; req.body.secret = req.session.register_client.secret; req.body.user = req.session.user; req.body.redirect_uris = req.body.redirect_uris.split(/[, ]+/); - req.model.client.create(req.body, function(err, client){ + req.model.client.create(req.body, function(err, client) { if(!err && client) { res.redirect('/client/'+client.id); } else { @@ -261,247 +285,260 @@ app.post('/client/register', oidc.use('client'), function(req, res, next) { }); }); -app.get('/client', oidc.use('client'), function(req, res, next){ - var head ='

    Clients Page

    Register new client
    '; - req.model.client.find({user: req.session.user}, function(err, clients){ - var body = ["'); - res.send(head+body.join('')); +app.get('/client', oidc.use('client'), function(req, res, next) { + /*jshint unused:false */ + var head = '

    Clients Page

    Register new client
    '; + req.model.client.find({user: req.session.user}, function(err, clients) { + var body = [''); + res.send(head+body.join('')); }); }); -app.get('/client/:id', oidc.use('client'), function(req, res, next){ - req.model.client.findOne({user: req.session.user, id: req.params.id}, function(err, client){ - if(err) { - next(err); - } else if(client) { - var html = '

    Client '+client.name+' Page

    Go back
    '; - res.send(html); - } else { - res.send('

    No Client Fount!

    Go back
    '); - } +app.get('/client/:id', oidc.use('client'), function(req, res, next) { + req.model.client.findOne({user: req.session.user, id: req.params.id}, function(err, client) { + if(err) { + next(err); + } else if(client) { + var html = '

    Client '+client.name+' Page

    Go back
    '; + res.send(html); + } else { + res.send('

    No Client Fount!

    Go back
    '); + } }); }); -app.get('/test/clear', function(req, res, next){ - test = {status: 'new'}; - res.redirect('/test'); +app.get('/test/clear', function(req, res, next) { + /*jshint unused:false */ + test = {status: 'new'}; + res.redirect('/test'); }); app.get('/test', oidc.use({policies: {loggedIn: false}, models: 'client'}), function(req, res, next) { - var html='

    Test Auth Flows

    '; - var resOps = { - "/user/foo": "Restricted by foo scope", - "/user/bar": "Restricted by bar scope", - "/user/and": "Restricted by 'bar and foo' scopes", - "/user/or": "Restricted by 'bar or foo' scopes", - "/api/user": "User Info Endpoint" - }; - var mkinputs = function(name, desc, type, value, options) { - var inp = ''; - switch(type) { - case 'select': - inp = ''; + for(var i in options) { + if(options.hasOwnProperty(i)) { + inp += ''; + } + } + inp += ''; + inp = '
    '+inp+'
    '; + break; + default: + if(options) { + for(var j in options) { + if(options.hasOwnProperty(j)) { + inp += '
    '+ + ''+ + ''+ + '
    '; } - inp += ''; + } + } else { + inp = ''; + if(type!='hidden') { inp = '
    '+inp+'
    '; - break; - default: - if(options) { - for(var i in options) { - inp += '
    '+ - ''+ - ''+ - '
    '; - } - } else { - inp = ''; - if(type!='hidden') { - inp = '
    '+inp+'
    '; - } - } + } } - return inp; - }; - switch(test.status) { - case "new": - req.model.client.find().populate('user').exec(function(err, clients){ - var inputs = []; - inputs.push(mkinputs('response_type', 'Auth Flow', 'select', null, {code: 'Auth Code', "id_token token": 'Implicit'})); - var options = {}; - clients.forEach(function(client){ - options[client.key+':'+client.secret]=client.user.id+' '+client.user.email+' '+client.key+' ('+client.redirect_uris.join(', ')+')'; - }); - inputs.push(mkinputs('client_id', 'Client Key', 'select', null, options)); - //inputs.push(mkinputs('secret', 'Client Secret', 'text')); - inputs.push(mkinputs('scope', 'Scopes', 'text')); - inputs.push(mkinputs('nonce', 'Nonce', 'text', 'N-'+Math.random())); - test.status='1'; - res.send(html+'
    '+inputs.join('')+'
    '); + } + return inp; + }; + switch(test.status) { + case 'new': + req.model.client.find().populate('user').exec(function(err, clients) { + var inputs = []; + inputs.push(mkinputs('response_type', 'Auth Flow', 'select', null, {code: 'Auth Code', 'id_token token': 'Implicit'})); + var options = {}; + clients.forEach(function(client) { + options[client.key+':'+client.secret] = client.user.id+' '+client.user.email+' '+client.key+' ('+client.redirect_uris.join(', ')+')'; }); - break; + inputs.push(mkinputs('client_id', 'Client Key', 'select', null, options)); + //inputs.push(mkinputs('secret', 'Client Secret', 'text')); + inputs.push(mkinputs('scope', 'Scopes', 'text')); + inputs.push(mkinputs('nonce', 'Nonce', 'text', 'N-'+Math.random())); + test.status = '1'; + res.send(html+'
    '+inputs.join('')+'
    '); + }); + break; case '1': - req.query.redirect_uri=req.protocol+'://'+req.headers.host+req.path; - extend(test, req.query); - req.query.client_id = req.query.client_id.split(':')[0]; - test.status = '2'; - res.redirect('/user/authorize?'+querystring.stringify(req.query)); - break; + req.query.redirect_uri = req.protocol+'://'+req.headers.host+req.path; + extend(test, req.query); + req.query.client_id = req.query.client_id.split(':')[0]; + test.status = '2'; + res.redirect('/user/authorize?'+querystring.stringify(req.query)); + break; case '2': - extend(test, req.query); - if(test.response_type == 'code') { - test.status = '3'; - var inputs = []; - //var c = test.client_id.split(':'); - inputs.push(mkinputs('code', 'Code', 'text', req.query.code)); - /*inputs.push(mkinputs('grant_type', null, 'hidden', 'authorization_code')); - inputs.push(mkinputs('client_id', null, 'hidden', c[0])); - inputs.push(mkinputs('client_secret', null, 'hidden', c[1])); - inputs.push(mkinputs('redirect_uri', null, 'hidden', test.redirect_uri));*/ - res.send(html+'
    '+inputs.join('')+'
    '); - } else { - test.status = '4'; - html += "Got:
    "; + extend(test, req.query); + var inputs; + if(test.response_type == 'code') { + test.status = '3'; + inputs = []; + //var c = test.client_id.split(':'); + inputs.push(mkinputs('code', 'Code', 'text', req.query.code)); + /*inputs.push(mkinputs('grant_type', null, 'hidden', 'authorization_code')); + inputs.push(mkinputs('client_id', null, 'hidden', c[0])); + inputs.push(mkinputs('client_secret', null, 'hidden', c[1])); + inputs.push(mkinputs('redirect_uri', null, 'hidden', test.redirect_uri));*/ + res.send(html+'
    '+inputs.join('')+'
    '); + } else { + test.status = '4'; + html += 'Got:
    '; + inputs = []; + //var c = test.client_id.split(':'); + inputs.push(mkinputs('access_token', 'Access Token', 'text')); + inputs.push(mkinputs('page', 'Resource to access', 'select', null, resOps)); + + var after = + ''; + /*inputs.push(mkinputs('grant_type', null, 'hidden', 'authorization_code')); + inputs.push(mkinputs('client_id', null, 'hidden', c[0])); + inputs.push(mkinputs('client_secret', null, 'hidden', c[1])); + inputs.push(mkinputs('redirect_uri', null, 'hidden', test.redirect_uri));*/ + res.send(html+'
    '+inputs.join('')+'
    '+after); + } + break; + case '3': + test.status = '4'; + + test.code = req.query.code; + var query = { + grant_type: 'authorization_code', + code: test.code, + redirect_uri: test.redirect_uri + }; + + var post_data = querystring.stringify(query); + var post_options = { + port: app.get('port'), + path: '/user/token', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': post_data.length, + 'Authorization': 'Basic '+new Buffer(test.client_id, 'utf8').toString('base64'), + 'Cookie': req.headers.cookie + } + }; + + // Set up the request + var post_req = http.request(post_options, function(pres) { + pres.setEncoding('utf8'); + var data = ''; + pres.on('data', function (chunk) { + data += chunk; + console.log('Response: '+chunk); + }); + pres.on('end', function() { + try { + data = JSON.parse(data); + html += 'Got:
    '+JSON.stringify(data)+'
    '; var inputs = []; //var c = test.client_id.split(':'); - inputs.push(mkinputs('access_token', 'Access Token', 'text')); + inputs.push(mkinputs('access_token', 'Access Token', 'text', data.access_token)); inputs.push(mkinputs('page', 'Resource to access', 'select', null, resOps)); - - var after = - ""; /*inputs.push(mkinputs('grant_type', null, 'hidden', 'authorization_code')); - inputs.push(mkinputs('client_id', null, 'hidden', c[0])); - inputs.push(mkinputs('client_secret', null, 'hidden', c[1])); - inputs.push(mkinputs('redirect_uri', null, 'hidden', test.redirect_uri));*/ - res.send(html+'
    '+inputs.join('')+'
    '+after); - } - break; - case '3': - test.status = '4'; - test.code = req.query.code; - var query = { - grant_type: 'authorization_code', - code: test.code, - redirect_uri: test.redirect_uri - }; - var post_data = querystring.stringify(query); - var post_options = { - port: app.get('port'), - path: '/user/token', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': post_data.length, - 'Authorization': 'Basic '+Buffer(test.client_id, 'utf8').toString('base64'), - 'Cookie': req.headers.cookie - } - }; - - // Set up the request - var post_req = http.request(post_options, function(pres) { - pres.setEncoding('utf8'); - var data = ''; - pres.on('data', function (chunk) { - data += chunk; - console.log('Response: ' + chunk); - }); - pres.on('end', function(){ - console.log(data); - try { - data = JSON.parse(data); - html += "Got:
    "+JSON.stringify(data)+"
    "; - var inputs = []; - //var c = test.client_id.split(':'); - inputs.push(mkinputs('access_token', 'Access Token', 'text', data.access_token)); - inputs.push(mkinputs('page', 'Resource to access', 'select', null, resOps)); - /*inputs.push(mkinputs('grant_type', null, 'hidden', 'authorization_code')); - inputs.push(mkinputs('client_id', null, 'hidden', c[0])); - inputs.push(mkinputs('client_secret', null, 'hidden', c[1])); - inputs.push(mkinputs('redirect_uri', null, 'hidden', test.redirect_uri));*/ - res.send(html+'
    '+inputs.join('')+'
    '); - } catch(e) { - res.send('
    '+data+'
    '); - } - }); + inputs.push(mkinputs('client_id', null, 'hidden', c[0])); + inputs.push(mkinputs('client_secret', null, 'hidden', c[1])); + inputs.push(mkinputs('redirect_uri', null, 'hidden', test.redirect_uri));*/ + res.send(html+'
    '+inputs.join('')+'
    '); + } catch(e) { + res.send('
    '+data+'
    '); + } }); + }); - // post the data - post_req.write(post_data); - post_req.end(); - break; -//res.redirect('/user/token?'+querystring.stringify(query)); + // post the data + post_req.write(post_data); + post_req.end(); + break; + //res.redirect('/user/token?'+querystring.stringify(query)); case '4': - test = {status: 'new'}; - res.redirect(req.query.page+'?access_token='+req.query.access_token); - } + test = {status: 'new'}; + res.redirect(req.query.page+'?access_token='+req.query.access_token); + } }); // development only -if ('development' == app.get('env')) { +if('development' == app.get('env')) { app.use(errorHandler()); } +/* function mkFields(params) { - var fields={}; + var fields = {}; for(var i in params) { if(params[i].html) { fields[i] = {}; fields[i].label = params[i].label||(i.charAt(0).toUpperCase()+i.slice(1)).replace(/_/g, ' '); switch(params[i].html) { - case 'password': - fields[i].html = ''; - break; - case 'date': - fields[i].html = ''; - break; - case 'hidden': - fields[i].html = ''; - fields[i].label = false; - break; - case 'fixed': - fields[i].html = ''+params[i].value+''; - break; - case 'radio': - fields[i].html = ''; - for(var j=0; j '+params[i].ops[j]; - } - break; - default: - fields[i].html = ''; - break; + case 'password': + fields[i].html = ''; + break; + case 'date': + fields[i].html = ''; + break; + case 'hidden': + fields[i].html = ''; + fields[i].label = false; + break; + case 'fixed': + fields[i].html = ''+params[i].value+''; + break; + case 'radio': + fields[i].html = ''; + for(var j = 0; j '+params[i].ops[j]; + } + break; + default: + fields[i].html = ''; + break; } } } return fields; } +//*/ - var clearErrors = function(req, res, next) { - delete req.session.error; - next(); - }; +/* +var clearErrors = function(req, res, next) { + delete req.session.error; + next(); +}; +//*/ -http.createServer(app).listen(app.get('port'), function(){ - console.log('Express server listening on port ' + app.get('port')); +http.createServer(app).listen(app.get('port'), function() { + console.log('Express server listening on port '+app.get('port')); }); diff --git a/index.js b/index.js index b41bdc1..3b9808b 100644 --- a/index.js +++ b/index.js @@ -16,358 +16,371 @@ crypto = require('crypto'), _ = require('lodash'), extend = require('extend'), url = require('url'), -Q = require('q'), +qPromise = require('q'), jwt = require('jwt-simple'), -util = require("util"), +util = require('util'), base64url = require('base64url'), cleanObj = require('clean-obj'); - var defaults = { - login_url: '/login', - consent_url: '/consent', - scopes: { - openid: 'Informs the Authorization Server that the Client is making an OpenID Connect request.', - profile:'Access to the End-User\'s default profile Claims.', - email: 'Access to the email and email_verified Claims.', - address: 'Access to the address Claim.', - phone: 'Access to the phone_number and phone_number_verified Claims.', - offline_access: 'Grants access to the End-User\'s UserInfo Endpoint even when the End-User is not present (not logged in).' - }, - policies:{ - loggedIn: function(req, res, next) { - if(req.session.user) { - next(); - } else { - var q = req.parsedParams?req.path+'?'+querystring.stringify(req.parsedParams):req.originalUrl; - res.redirect(this.settings.login_url+'?'+querystring.stringify({return_url: q})); - } - }, - }, - adapters: { - redis: sailsRedis - }, - connections: { - def: { - adapter: 'redis' - } + login_url: '/login', + consent_url: '/consent', + scopes: { + openid: 'Informs the Authorization Server that the Client is making an OpenID Connect request.', + profile:'Access to the End-User\'s default profile Claims.', + email: 'Access to the email and email_verified Claims.', + address: 'Access to the address Claim.', + phone: 'Access to the phone_number and phone_number_verified Claims.', + offline_access: 'Grants access to the End-User\'s UserInfo Endpoint even when the End-User is not present (not logged in).' + }, + policies:{ + loggedIn: function(req, res, next) { + if(req.session.user) { + next(); + } else { + var q = req.parsedParams?req.path+'?'+querystring.stringify(req.parsedParams):req.originalUrl; + res.redirect(this.settings.login_url+'?'+querystring.stringify({return_url: q})); + } + }, + }, + adapters: { + redis: sailsRedis + }, + connections: { + def: { + adapter: 'redis' + } + }, + models: { + user: { + identity: 'user', + connection: 'def', + schema: true, + policies: 'loggedIn', + attributes: { + name: {type: 'string', required: true, unique: true}, + given_name: {type: 'string', required: true}, + middle_name: 'string', + family_name: {type: 'string', required: true}, + profile: 'string', + email: {type: 'string', email: true, required: true, unique: true}, + password: 'string', + picture: 'binary', + birthdate: 'date', + gender: 'string', + phone_number: 'string', + samePassword: function(clearText) { + var sha256 = crypto.createHash('sha256'); + sha256.update(clearText); + return this.password == sha256.digest('hex'); + } + }, + beforeCreate: function(values, next) { + if(values.password) { + if(values.password!=values.passConfirm) { + return next('Password and confirmation does not match'); + } + var sha256 = crypto.createHash('sha256'); + sha256.update(values.password); + values.password = sha256.digest('hex'); + } + next(); + }, + beforeUpdate: function(values, next) { + if(values.password) { + if(values.password!=values.passConfirm) { + return next('Password and confirmation does not match'); + } + var sha256 = crypto.createHash('sha256'); + sha256.update(values.password); + values.password = sha256.digest('hex'); + } + next(); + } + }, + client: { + identity: 'client', + connection: 'def', + schema: true, + policies: 'loggedIn', + attributes: { + key: {type: 'string', required: true, unique: true}, + secret: {type: 'string', required: true, unique: true}, + name: {type: 'string', required: true}, + image: 'binary', + user: {model: 'user'}, + redirect_uris: {type:'array', required: true}, + credentialsFlow: {type: 'boolean', defaultsTo: false} + }, + beforeCreate: function(values, next) { + var sha256 = crypto.createHash('sha256'); + if(!values.key) { + sha256.update(values.name); + sha256.update(Math.random()+''); + values.key = sha256.digest('hex'); + } else if(!values.secret) { + sha256.update(values.key); + sha256.update(values.name); + sha256.update(Math.random()+''); + values.secret = sha256.digest('hex'); + } + next(); + } + }, + consent: { + identity: 'consent', + connection: 'def', + policies: 'loggedIn', + attributes: { + user: {model: 'user', required: true}, + client: {model: 'client', required: true}, + scopes: 'array' + } + }, + auth: { + identity: 'auth', + connection: 'def', + policies: 'loggedIn', + attributes: { + client: {model: 'client', required: true}, + scope: {type: 'array', required: true}, + user: {model: 'user', required: true}, + sub: {type: 'string', required: true}, + code: {type: 'string', required: true}, + redirectUri: {type: 'url', required: true}, + responseType: {type: 'string', required: true}, + status: {type: 'string', required: true}, + accessTokens: { + collection: 'access', + via: 'auth' }, - models: { - user: { - identity: 'user', - connection: 'def', - schema: true, - policies: 'loggedIn', - attributes: { - name: {type: 'string', required: true, unique: true}, - given_name: {type: 'string', required: true}, - middle_name: 'string', - family_name: {type: 'string', required: true}, - profile: 'string', - email: {type: 'string', email: true, required: true, unique: true}, - password: 'string', - picture: 'binary', - birthdate: 'date', - gender: 'string', - phone_number: 'string', - samePassword: function(clearText) { - var sha256 = crypto.createHash('sha256'); - sha256.update(clearText); - return this.password == sha256.digest('hex'); - } - }, - beforeCreate: function(values, next) { - if(values.password) { - if(values.password != values.passConfirm) { - return next("Password and confirmation does not match"); - } - var sha256 = crypto.createHash('sha256'); - sha256.update(values.password); - values.password = sha256.digest('hex'); - } - next(); - }, - beforeUpdate: function(values, next) { - if(values.password) { - if(values.password != values.passConfirm) { - return next("Password and confirmation does not match"); - } - var sha256 = crypto.createHash('sha256'); - sha256.update(values.password); - values.password = sha256.digest('hex'); - } - next(); - } - }, - client: { - identity: 'client', - connection: 'def', - schema: true, - policies: 'loggedIn', - attributes: { - key: {type: 'string', required: true, unique: true}, - secret: {type: 'string', required: true, unique: true}, - name: {type: 'string', required: true}, - image: 'binary', - user: {model: 'user'}, - redirect_uris: {type:'array', required: true}, - credentialsFlow: {type: 'boolean', defaultsTo: false} - }, - beforeCreate: function(values, next) { - if(!values.key) { - var sha256 = crypto.createHash('sha256'); - sha256.update(values.name); - sha256.update(Math.random()+''); - values.key = sha256.digest('hex'); - } - if(!values.secret) { - var sha256 = crypto.createHash('sha256'); - sha256.update(values.key); - sha256.update(values.name); - sha256.update(Math.random()+''); - values.secret = sha256.digest('hex'); - } - next(); - } - }, - consent: { - identity: 'consent', - connection: 'def', - policies: 'loggedIn', - attributes: { - user: {model: 'user', required: true}, - client: {model: 'client', required: true}, - scopes: 'array' - } - }, - auth: { - identity: 'auth', - connection: 'def', - policies: 'loggedIn', - attributes: { - client: {model: 'client', required: true}, - scope: {type: 'array', required: true}, - user: {model: 'user', required: true}, - sub: {type: 'string', required: true}, - code: {type: 'string', required: true}, - redirectUri: {type: 'url', required: true}, - responseType: {type: 'string', required: true}, - status: {type: 'string', required: true}, - accessTokens: { - collection: 'access', - via: 'auth' - }, - refreshTokens: { - collection: 'refresh', - via: 'auth' - } - } - }, - access: { - identity: 'access', - connection: 'def', - attributes: { - token: {type: 'string', required: true}, - type: {type: 'string', required: true}, - idToken: 'string', - expiresIn: 'integer', - scope: {type: 'array', required: true}, - client: {model: 'client', required: true}, - user: {model: 'user', required: true}, - auth: {model: 'auth'} - } - }, - refresh: { - identity: 'refresh', - connection: 'def', - attributes: { - token: {type: 'string', required: true}, - scope: {type: 'array', required: true}, - auth: {model: 'auth', required: true}, - status: {type: 'string', required: true} - } - } + refreshTokens: { + collection: 'refresh', + via: 'auth' } + } + }, + access: { + identity: 'access', + connection: 'def', + attributes: { + token: {type: 'string', required: true}, + type: {type: 'string', required: true}, + idToken: 'string', + expiresIn: 'integer', + scope: {type: 'array', required: true}, + client: {model: 'client', required: true}, + user: {model: 'user', required: true}, + auth: {model: 'auth'} + } + }, + refresh: { + identity: 'refresh', + connection: 'def', + attributes: { + token: {type: 'string', required: true}, + scope: {type: 'array', required: true}, + // TODO when this line is required, it results in + // an error being thrown when clicking the "Get + // Token" button on the "Test Auth Flows" page. + //auth: {model: 'auth', required: true}, + auth: {model: 'auth'}, + status: {type: 'string', required: true} + } + } + } }; function parse_authorization(authorization) { - if(!authorization) - return null; + if(!authorization) + return null; - var parts = authorization.split(' '); + var parts = authorization.split(' '); - if(parts.length != 2 || parts[0] != 'Basic') - return null; + if(parts.length!=2|| parts[0]!='Basic') + return null; - var creds = new Buffer(parts[1], 'base64').toString(), - i = creds.indexOf(':'); + var creds = new Buffer(parts[1], 'base64').toString(), + i = creds.indexOf(':'); - if(i == -1) - return null; + if(i == -1) + return null; - var username = creds.slice(0, i); - password = creds.slice(i + 1); + var username = creds.slice(0, i); + var password = creds.slice(i+1); - return [username, password]; + return [username, password]; } function OpenIDConnect(options) { - this.settings = extend(true, {}, defaults, options); + this.settings = extend(true, {}, defaults, options); - //allow removing attributes, by marking thme as null - cleanObj(this.settings.models, true); + //allow removing attributes, by marking thme as null + cleanObj(this.settings.models, true); - for(var i in this.settings.policies) { - this.settings.policies[i] = this.settings.policies[i].bind(this); + for(var i in this.settings.policies) { + if(this.settings.policies.hasOwnProperty(i)) { + this.settings.policies[i] = this.settings.policies[i].bind(this); } + } - if(this.settings.alien) { - for(var i in alien) { - if(this.settings.models[i]) delete this.settings.models[i]; - } + if(this.settings.alien) { + for(var j in this.settings.alien) { + if(this.settings.alien.hasOwnProperty(j)) { + if(this.settings.models[j]) delete this.settings.models[j]; + } } - - if(this.settings.orm) { - this.orm = this.settings.orm; - for(var i in this.settings.policies) { - this.orm.setPolicy(true, i, this.settings.policies[i]); - } - } else { - - this.orm = new modelling({ - models: this.settings.models, - adapters: this.settings.adapters, - connections: this.settings.connections, - app: this.settings.app, - policies: this.settings.policies - }); + } + + if(this.settings.orm) { + this.orm = this.settings.orm; + for(var k in this.settings.policies) { + if(this.settings.policies.hasOwnProperty(k)) { + this.orm.setPolicy(true, k, this.settings.policies[k]); + } } + } else { + this.orm = new modelling({ + models: this.settings.models, + adapters: this.settings.adapters, + connections: this.settings.connections, + app: this.settings.app, + policies: this.settings.policies + }); + } } OpenIDConnect.prototype = new EventEmitter(); OpenIDConnect.prototype.done = function() { - this.orm.done(); + this.orm.done(); }; OpenIDConnect.prototype.model = function(name) { - return this.orm.model(name); -} + return this.orm.model(name); +}; OpenIDConnect.prototype.use = function(name) { - var alien = {}; - if(this.settings.alien) { - var self = this; - if(!name) { - alien = this.settings.alien; - } else { - var m; - if(_.isPlainObject(name) && name.models) { - m = name.models; - } - if(util.isArray(m||name)) { - (m||name).forEach(function(model) { - if(self.settings.alien[model]) { - alien[model] = self.settings.alien[model]; - } - }); - } else if(self.settings.alien[m||name]) { - alien[m||name] = self.settings.alien[m||name]; - } - } + var alien = {}; + if(this.settings.alien) { + var self = this; + if(!name) { + alien = this.settings.alien; + } else { + var m; + if(_.isPlainObject(name) && name.models) { + m = name.models; + } + if(util.isArray(m||name)) { + (m||name).forEach(function(model) { + if(self.settings.alien[model]) { + alien[model] = self.settings.alien[model]; + } + }); + } else if(self.settings.alien[m||name]) { + alien[m||name] = self.settings.alien[m||name]; + } } - return [this.orm.use(name), function(req, res, next) { - extend(req.model, alien); - next(); - }]; + } + return [this.orm.use(name), function(req, res, next) { + extend(req.model, alien); + next(); + }]; }; OpenIDConnect.prototype.getOrm = function() { - return this.orm; -} + return this.orm; +}; /*OpenIDConnect.prototype.getClientParams = function() { - return this.orm.client.getParams(); + return this.orm.client.getParams(); };*/ /*OpenIDConnect.prototype.searchClient = function(parts, callback) { - return new this.orm.client.reverse(parts, callback); + return new this.orm.client.reverse(parts, callback); }; OpenIDConnect.prototype.getUserParams = function() { - return this.orm.user.getParams(); + return this.orm.user.getParams(); }; OpenIDConnect.prototype.user = function(params, callback) { - return new this.orm.user(params, callback); + return new this.orm.user(params, callback); }; OpenIDConnect.prototype.searchUser = function(parts, callback) { - return new this.orm.user.reverse(parts, callback); + return new this.orm.user.reverse(parts, callback); };*/ OpenIDConnect.prototype.errorHandle = function(res, uri, error, desc) { - if(uri) { - var redirect = url.parse(uri,true); - redirect.query.error = error; //'invalid_request'; - redirect.query.error_description = desc; //'Parameter '+x+' is mandatory.'; - res.redirect(400, url.format(redirect)); - } else { - res.send(400, error+': '+desc); - } + if(uri) { + var redirect = url.parse(uri, true); + redirect.query.error = error; //'invalid_request'; + redirect.query.error_description = desc; //'Parameter '+x+' is mandatory.'; + res.redirect(400, url.format(redirect)); + } else { + res.send(400, error+': '+desc); + } }; OpenIDConnect.prototype.endpointParams = function (spec, req, res, next) { - try { - req.parsedParams = this.parseParams(req, res, spec); - next(); - } catch(err) { - this.errorHandle(res, err.uri, err.error, err.msg); - } -} + try { + req.parsedParams = this.parseParams(req, res, spec); + next(); + } catch(err) { + this.errorHandle(res, err.uri, err.error, err.msg); + } +}; OpenIDConnect.prototype.parseParams = function(req, res, spec) { - var params = {}; - var r = req.param('redirect_uri'); - for(var i in spec) { - var x = req.param(i); - if(x) { - params[i] = x; - } + var params = {}; + var r = req.param('redirect_uri'); + for(var i in spec) { + if(spec.hasOwnProperty(i)) { + var x = req.param(i); + if(x) { + params[i] = x; + } } - - for(var i in spec) { - var x = params[i]; - if(!x) { - var error = false; - if(typeof spec[i] == 'boolean') { - error = spec[i]; - } else if (_.isPlainObject(spec[i])) { - for(var j in spec[i]) { - if(!util.isArray(spec[i][j])) { - spec[i][j] = [spec[i][j]]; - } - spec[i][j].forEach(function(e) { - if(!error) { - if(util.isRegExp(e)) { - error = e.test(params[j]); - } else { - error = e == params[j]; - } - } - }); + } + + for(var i2 in spec) { + if(spec.hasOwnProperty(i2)) { + var x2 = params[i2]; + if(!x2) { + var error = false; + if(typeof spec[i2] == 'boolean') { + error = spec[i2]; + } else if(_.isPlainObject(spec[i2])) { + for(var j in spec[i2]) { + if(spec.hasOwnProperty(j)) { + if(!util.isArray(spec[i2][j])) { + spec[i2][j] = [spec[i2][j]]; + } + /*jshint -W083 */ + spec[i2][j].forEach(function(e) { + if(!error) { + if(util.isRegExp(e)) { + error = e.test(params[j]); + } else { + error = e == params[j]; + } } - } else if (_.isFunction(spec[i])) { - error = spec[i](params); + }); } + } + } else if(_.isFunction(spec[i2])) { + error = spec[i2](params); + } - if(error) { - throw {type: 'error', uri: r, error: 'invalid_request', msg: 'Parameter '+i+' is mandatory.'}; - //this.errorHandle(res, r, 'invalid_request', 'Parameter '+i+' is mandatory.'); - //return; - } + if(error) { + throw {type: 'error', uri: r, error: 'invalid_request', msg: 'Parameter '+i2+' is mandatory.'}; + //this.errorHandle(res, r, 'invalid_request', 'Parameter '+i2+' is mandatory.'); + //return; } + } } - return params; + } + return params; }; /** @@ -384,35 +397,35 @@ OpenIDConnect.prototype.parseParams = function(req, res, spec) { */ OpenIDConnect.prototype.login = function(validateUser) { - var self = this; - - return [self.use({policies: {loggedIn: false}, models: 'user'}), - function(req, res, next) { - validateUser(req, /*next:*/function(error,user) { - if(!error && !user) { - error = new Error('User not validated'); - } - if(!error) { - if(user.id) { - req.session.user = user.id; - } else { - delete req.session.user; - } - if(user.sub) { - if(typeof user.sub ==='function') { - req.session.sub = user.sub(); - } else { - req.session.sub = user.sub; - } - } else { - delete req.session.sub; - } - return next(); - } else { - return next(error); - } - }); - }]; + var self = this; + + return [self.use({policies: {loggedIn: false}, models: 'user'}), + function(req, res, next) { + validateUser(req, /*next:*/function(error,user) { + if(!error && !user) { + error = new Error('User not validated'); + } + if(!error) { + if(user.id) { + req.session.user = user.id; + } else { + delete req.session.user; + } + if(user.sub) { + if(typeof user.sub === 'function') { + req.session.sub = user.sub(); + } else { + req.session.sub = user.sub; + } + } else { + delete req.session.sub; + } + return next(); + } else { + return next(error); + } + }); + }]; }; /** @@ -426,250 +439,254 @@ OpenIDConnect.prototype.login = function(validateUser) { * */ OpenIDConnect.prototype.auth = function() { - var self = this; - var spec = { - response_type: true, - client_id: true, - scope: true, - redirect_uri: true, - state: false, - nonce: function(params){ - return params.response_type.indexOf('id_token')!==-1; - }, - display: false, - prompt: false, - max_age: false, - ui_locales: false, - claims_locales: false, - id_token_hint: false, - login_hint: false, - acr_values: false, - response_mode: false - }; - return [function(req, res, next) { - self.endpointParams(spec, req, res, next); - }, - self.use(['client', 'consent', 'auth', 'access']), - function(req, res, next) { - Q(req.parsedParams).then(function(params) { - //Step 2: Check if response_type is supported and client_id is valid. - - var deferred = Q.defer(); - switch(params.response_type) { - case 'none': - case 'code': - case 'token': - case 'id_token': - break; - default: - //var error = false; - var sp = params.response_type.split(' '); - sp.forEach(function(response_type) { - if(['code', 'token', 'id_token'].indexOf(response_type) == -1) { - throw {type: 'error', uri: params.redirect_uri, error: 'unsupported_response_type', msg: 'Response type '+response_type+' not supported.'}; - } - }); - } - req.model.client.findOne({key: params.client_id}, function(err, model) { - if(err || !model || model === '') { - deferred.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_client', msg: 'Client '+params.client_id+' doesn\'t exist.'}); - } else { - req.session.client_id = model.id; - req.session.client_secret = model.secret; - deferred.resolve(params); - } - }); + var self = this; + var spec = { + response_type: true, + client_id: true, + scope: true, + redirect_uri: true, + state: false, + nonce: function(params) { + return params.response_type.indexOf('id_token') !== -1; + }, + display: false, + prompt: false, + max_age: false, + ui_locales: false, + claims_locales: false, + id_token_hint: false, + login_hint: false, + acr_values: false, + response_mode: false + }; + return [ + function(req, res, next) { + self.endpointParams(spec, req, res, next); + }, + self.use(['client', 'consent', 'auth', 'access']), + function(req, res, next) { + /*jshint unused:false */ + qPromise(req.parsedParams).then(function(params) { + //Step 2: Check if response_type is supported and client_id is valid. + + var deferred = qPromise.defer(); + switch(params.response_type) { + case 'none': + case 'code': + case 'token': + case 'id_token': + break; + default: + //var error = false; + var sp = params.response_type.split(' '); + sp.forEach(function(response_type) { + if(['code', 'token', 'id_token'].indexOf(response_type) == -1) { + throw {type: 'error', uri: params.redirect_uri, error: 'unsupported_response_type', msg: 'Response type '+response_type+' not supported.'}; + } + }); + } + req.model.client.findOne({key: params.client_id}, function(err, model) { + if(err || !model || model === '') { + deferred.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_client', msg: 'Client '+params.client_id+' doesn\'t exist.'}); + } else { + req.session.client_id = model.id; + req.session.client_secret = model.secret; + deferred.resolve(params); + } + }); - return deferred.promise; - }).then(function(params){ - //Step 3: Check if scopes are valid, and if consent was given. - - var deferred = Q.defer(); - var reqsco = params.scope.split(' '); - req.session.scopes = {}; - var promises = []; - req.model.consent.findOne({user: req.session.user, client: req.session.client_id}, function(err, consent) { - reqsco.forEach(function(scope) { - var innerDef = Q.defer(); - if(!self.settings.scopes[scope]) { - innerDef.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_scope', msg: 'Scope '+scope+' not supported.'}); - } - if(!consent) { - req.session.scopes[scope] = {ismember: false, explain: self.settings.scopes[scope]}; - innerDef.resolve(true); - } else { - var inScope = consent.scopes.indexOf(scope) !== -1; - req.session.scopes[scope] = {ismember: inScope, explain: self.settings.scopes[scope]}; - innerDef.resolve(!inScope); - } - promises.push(innerDef.promise); - }); - - Q.allSettled(promises).then(function(results){ - var redirect = false; - for(var i = 0; i 1) { - var last = errors.pop(); - self.errorHandle(res, null, 'invalid_scope', 'Required scopes '+errors.join(', ')+' and '+last+' where not granted.'); - } else if(errors.length > 0) { - self.errorHandle(res, null, 'invalid_scope', 'Required scope '+errors.pop()+' not granted.'); - } else { - req.check = req.check||{}; - req.check.scopes = access.scope; - next(); - } - } else { - self.errorHandle(res, null, 'unauthorized_client', 'Access token is not valid.'); - } + //Seguir desde acá!!!! + var scopes = Array.prototype.slice.call(arguments, 0); + if(!util.isArray(scopes)) { + scopes = [scopes]; + } + var self = this; + var spec = { + access_token: false + }; + + return [ + function(req, res, next) { + self.endpointParams(spec, req, res, next); + }, + self.use({policies: {loggedIn: false}, models:['access', 'auth']}), + function(req, res, next) { + var params = req.parsedParams; + if(!scopes.length) { + next(); + } else { + if(!params.access_token) { + params.access_token = (req.headers.authorization || '').indexOf('Bearer ') === 0 ? + req.headers.authorization.replace('Bearer', '').trim():false; + } + if(params.access_token) { + req.model.access.findOne({token: params.access_token}) + .exec(function(err, access) { + if(!err && access) { + var errors = []; + + scopes.forEach(function(scope) { + if(typeof scope == 'string') { + if(access.scope.indexOf(scope) == -1) { + errors.push(scope); + } + } else if(util.isRegExp(scope)) { + var inS = false; + access.scope.forEach(function(s) { + if(scope.test(s)) { + inS = true; + } }); + if(!inS) { + errors.push('('+scope.toString().replace(/\//g, '')+')'); + } + } + }); + if(errors.length > 1) { + var last = errors.pop(); + self.errorHandle(res, null, 'invalid_scope', 'Required scopes '+errors.join(', ')+' and '+last+' where not granted.'); + } else if(errors.length > 0) { + self.errorHandle(res, null, 'invalid_scope', 'Required scope '+errors.pop()+' not granted.'); } else { - self.errorHandle(res, null, 'unauthorized_client', 'No access token found.'); + req.check = req.check||{}; + req.check.scopes = access.scope; + next(); } - } + } else { + self.errorHandle(res, null, 'unauthorized_client', 'Access token is not valid.'); + } + }); + } else { + self.errorHandle(res, null, 'unauthorized_client', 'No access token found.'); } - ]; + } + } + ]; }; /** @@ -1077,24 +1109,26 @@ OpenIDConnect.prototype.check = function() { * This function returns the user info in a json object. Checks for scope and login are included. */ OpenIDConnect.prototype.userInfo = function() { - var self = this; - return [ - self.check('openid', /profile|email/), - self.use('user'), - function(req, res, next) { - req.model.user.findOne({id: req.session.user}, function(err, user) { - if(req.check.scopes.indexOf('profile') != -1) { - user.sub = req.session.sub||req.session.user; - delete user.id; - delete user.password; - delete user.openidProvider; - res.json(user); - } else { - res.json({email: user.email}); - } - }); - } - ]; + var self = this; + return [ + self.check('openid', /profile|email/), + self.use('user'), + + function(req, res, next) { + /*jshint unused:false */ + req.model.user.findOne({id: req.session.user}, function(err, user) { + if(req.check.scopes.indexOf('profile')!=-1) { + user.sub = req.session.sub||req.session.user; + delete user.id; + delete user.password; + delete user.openidProvider; + res.json(user); + } else { + res.json({email: user.email}); + } + }); + } + ]; }; /** @@ -1108,65 +1142,66 @@ OpenIDConnect.prototype.userInfo = function() { * access_token is required either as a parameter or as a Bearer token */ OpenIDConnect.prototype.removetokens = function() { - var self = this, - spec = { - access_token: false //parameter not mandatory - }; - - return [ - function(req, res, next) { - self.endpointParams(spec, req, res, next); - }, - self.use({policies: {loggedIn: false}, models: ['access','auth']}), - function(req, res, next) { - var params = req.parsedParams; + var self = this, + spec = { + access_token: false //parameter not mandatory + }; - if(!params.access_token) { - params.access_token = (req.headers['authorization'] || '').indexOf('Bearer ') === 0 ? req.headers['authorization'].replace('Bearer', '').trim() : false; - } - if(params.access_token) { - //Delete the provided access token, and other tokens issued to the user - req.model.access.findOne({token: params.access_token}) - .exec(function(err, access) { - if(!err && access) { - req.model.auth.findOne({user: access.user}) - .populate('accessTokens') - .populate('refreshTokens') - .exec(function(err, auth) { - if(!err && auth) { - auth.accessTokens.forEach(function(access){ - access.destroy(); - }); - auth.refreshTokens.forEach(function(refresh){ - refresh.destroy(); - }); - auth.destroy(); - }; - req.model.access.find({user:access.user}) - .exec(function(err,accesses){ - if(!err && accesses) { - accesses.forEach(function(access) { - access.destroy(); - }); - }; - return next(); - }); - }); - } else { - self.errorHandle(res, null, 'unauthorized_client', 'Access token is not valid.'); - } - }); - } else { - self.errorHandle(res, null, 'unauthorized_client', 'No access token found.'); + return [ + function(req, res, next) { + self.endpointParams(spec, req, res, next); + }, + self.use({policies: {loggedIn: false}, models: ['access','auth']}), + function(req, res, next) { + var params = req.parsedParams; + + if(!params.access_token) { + params.access_token = (req.headers.authorization || '').indexOf('Bearer ') === 0 ? + req.headers.authorization.replace('Bearer', '').trim():false; + } + if(params.access_token) { + //Delete the provided access token, and other tokens issued to the user + req.model.access.findOne({token: params.access_token}) + .exec(function(err, access) { + if(!err && access) { + req.model.auth.findOne({user: access.user}) + .populate('accessTokens') + .populate('refreshTokens') + .exec(function(err, auth) { + if(!err && auth) { + auth.accessTokens.forEach(function(access) { + access.destroy(); + }); + auth.refreshTokens.forEach(function(refresh) { + refresh.destroy(); + }); + auth.destroy(); + } + req.model.access.find({user:access.user}) + .exec(function(err,accesses) { + if(!err && accesses) { + accesses.forEach(function(access) { + access.destroy(); + }); } + return next(); + }); + }); + } else { + self.errorHandle(res, null, 'unauthorized_client', 'Access token is not valid.'); } - ]; + }); + } else { + self.errorHandle(res, null, 'unauthorized_client', 'No access token found.'); + } + } + ]; }; exports.oidc = function(options) { - return new OpenIDConnect(options); + return new OpenIDConnect(options); }; exports.defaults = function() { - return defaults; -} + return defaults; +};