From d76133bfec7236d186d9d09533cdeea10878d228 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 1 Mar 2015 11:33:39 -0800 Subject: [PATCH 01/13] Added a rough example of a client and server working together --- examples/README.md | 27 + examples/client/example-app.js | 198 +++++++ examples/client/example-client.js | 113 ++++ examples/client/package.json | 18 + ...t-example.js => example-using-sessions.js} | 6 +- examples/example-using-tokens.js | 511 ++++++++++++++++++ 6 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 examples/README.md create mode 100644 examples/client/example-app.js create mode 100644 examples/client/example-client.js create mode 100644 examples/client/package.json rename examples/{openid-connect-example.js => example-using-sessions.js} (99%) create mode 100644 examples/example-using-tokens.js diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..304cf0e --- /dev/null +++ b/examples/README.md @@ -0,0 +1,27 @@ +# 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 by navigating to http://localhost:3001/user/create diff --git a/examples/client/example-app.js b/examples/client/example-app.js new file mode 100644 index 0000000..51f9776 --- /dev/null +++ b/examples/client/example-app.js @@ -0,0 +1,198 @@ +/** + * Module dependencies. + */ + +var bodyParser = require('body-parser'); +var express = require('express'); +var http = require('http'); +var logger = require('morgan'); +var passport = require('passport'); +var openid = require('passport-openidconnect'); + +var app = module.exports = express(); + +/* +passport.use(new LocalStrategy( + function(username, password, done) { + User.findOne({ username: username }, function (err, user) { + if (err) { return done(err); } + if (!user) { return done(null, false); } + if (!user.verifyPassword(password)) { return done(null, false); } + return done(null, user); + }); + } +)); +//*/ + +passport.use(new openid.Strategy({ + authorizationURL: 'http://localhost:3001/user/authorize', + tokenURL: 'http://localhost:3001/user/token', + clientID: '7a956c6a0e62f4b961d73b88de501fee', + clientSecret: '4d74027532ceba778aa280d5f620f152', + callbackURL: 'http://localhost:3000/users', + userInfoURL: 'http://localhost:3000/users/1' +}, +function(accessToken, refreshToken, profile, done) { + console.log('accessToken'); + console.log(accessToken); + console.log('refreshToken'); + console.log(refreshToken); + console.log('profile'); + console.log(profile); + console.log('done'); + console.log(done); + var loggedinUser = { + oauthID: profile.id, + name: profile.displayName, + created: Date.now() + }; + done(null, loggedinUser); + /* + User.findOne({oauthID: profile.id}, function(err, user) { + if(err) { console.log(err); } + if (!err && user != null) { + done(null, user); + } else { + var user = new User({ + oauthID: profile.id, + name: profile.displayName, + created: Date.now() + }); + user.save(function(err) { + if(err) { + console.log(err); + } else { + console.log("saving user ..."); + done(null, user); + }; + }); + }; + }); + //*/ +} +/* +function(err, result) { + console.log(err); + console.log(result); + console.log('Hello world'); + return false; +} +//*/ +)); + +app.set('port', process.env.PORT || 3000); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({extended: false})); +//app.use(express.session({secret: 'keyboard cat'})); +app.use(passport.initialize()); + +/* +app.configure(function() { + app.use(express.static(__dirname + '/../../public')); + app.use(express.cookieParser()); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({extended: false})); + app.use(express.session({secret: 'keyboard cat'})); + app.use(passport.initialize()); + app.use(passport.session()); +}); +//*/ + +// Ad-hoc example resource method + +app.resource = function(path, obj) { + this.get(path, + passport.authenticate('openidconnect', { session: false }), // THIS WORKS!! + obj.index); + this.get(path + '/:a..:b.:format?', function(req, res){ + var a = parseInt(req.params.a, 10); + var b = parseInt(req.params.b, 10); + var format = req.params.format; + obj.range(req, res, a, b, format); + }); + this.get(path + '/:id', obj.show); + this.delete(path + '/:id', + function(req, res){ + var id = parseInt(req.params.id, 10); + obj.destroy(req, res, id); + }); +}; + +// Fake records + +var users = [ + { name: 'tj' } + , { name: 'ciaran' } + , { name: 'aaron' } + , { name: 'guillermo' } + , { name: 'simon' } + , { name: 'tobi' } +]; + +// Fake controller. + +var User = { + index: function(req, res){ + res.send(users); + }, + show: function(req, res){ + res.send(users[req.params.id] || { error: 'Cannot find user' }); + }, + destroy: function(req, res, id){ + var destroyed = id in users; + delete users[id]; + res.send(destroyed ? 'destroyed' : 'Cannot find user'); + }, + range: function(req, res, a, b, format){ + var range = users.slice(a, b + 1); + switch (format) { + case 'json': + res.send(range); + break; + case 'html': + default: + var html = ''; + res.send(html); + break; + } + } +}; + +// curl http://localhost:3000/users -- responds with all users +// curl http://localhost:3000/users/1 -- responds with user 1 +// curl http://localhost:3000/users/4 -- responds with error +// curl http://localhost:3000/users/1..3 -- responds with several users +// curl -X DELETE http://localhost:3000/users/1 -- deletes the user + +app.resource('/users', + User); + +app.get('/', + passport.authenticate('openidconnect', { session: false }), // THIS WORKS!! + function(req, res){ + res.send([ + '

Examples:

' + ].join('\n')); + }); + +/* istanbul ignore next */ +/* +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} +//*/ + +http.createServer(app).listen(app.get('port'), function(){ + console.log('Express server listening on port ' + app.get('port')); +}); diff --git a/examples/client/example-client.js b/examples/client/example-client.js new file mode 100644 index 0000000..5b974fc --- /dev/null +++ b/examples/client/example-client.js @@ -0,0 +1,113 @@ +/** + * Module dependencies. + */ + +var express = require('express'); + +var app = module.exports = express(); + +// create an error with .status. we +// can then use the property in our +// custom error handler (Connect repects this prop as well) + +function error(status, msg) { + var err = new Error(msg); + err.status = status; + return err; +} + +// if we wanted to supply more than JSON, we could +// use something similar to the content-negotiation +// example. + +// here we validate the API key, +// by mounting this middleware to /api +// meaning only paths prefixed with "/api" +// will cause this middleware to be invoked + +app.use('/api', function(req, res, next){ + var key = req.query['api-key']; + + // key isn't present + if (!key) return next(error(400, 'api key required')); + + // key is invalid + if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key')); + + // all good, store req.key for route access + req.key = key; + next(); +}); + +// map of valid api keys, typically mapped to +// account info with some sort of database like redis. +// api keys do _not_ serve as authentication, merely to +// track API usage or help prevent malicious behavior etc. + +var apiKeys = ['foo', 'bar', 'baz']; + +// these two objects will serve as our faux database + +var repos = [ + { name: 'express', url: 'http://github.com/strongloop/express' } + , { name: 'stylus', url: 'http://github.com/learnboost/stylus' } + , { name: 'cluster', url: 'http://github.com/learnboost/cluster' } +]; + +var users = [ + { name: 'tobi' } + , { name: 'loki' } + , { name: 'jane' } +]; + +var userRepos = { + tobi: [repos[0], repos[1]] + , loki: [repos[1]] + , jane: [repos[2]] +}; + +// we now can assume the api key is valid, +// and simply expose the data + +// test with curl -i -H "Accept: application/json" localhost:3000/api/users?api-key=foo +app.get('/api/users', function(req, res, next){ + res.send(users); +}); + +app.get('/api/repos', function(req, res, next){ + res.send(repos); +}); + +app.get('/api/user/:name/repos', function(req, res, next){ + var name = req.params.name; + var user = userRepos[name]; + + if (user) res.send(user); + else next(); +}); + +// middleware with an arity of 4 are considered +// error handling middleware. When you next(err) +// it will be passed through the defined middleware +// in order, but ONLY those with an arity of 4, ignoring +// regular middleware. +app.use(function(err, req, res, next){ + // whatever you want here, feel free to populate + // properties on `err` to treat it differently in here. + res.status(err.status || 500); + res.send({ error: err.message }); +}); + +// our custom JSON 404 middleware. Since it's placed last +// it will be the last middleware called, if all others +// invoke next() and do not respond. +app.use(function(req, res){ + res.status(404); + res.send({ error: "Lame, can't find that" }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/examples/client/package.json b/examples/client/package.json new file mode 100644 index 0000000..d6e6e68 --- /dev/null +++ b/examples/client/package.json @@ -0,0 +1,18 @@ +{ + "name": "openid-connect-client-example", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "node example-client.js" + }, + "dependencies": { + "body-parser": "*", + "errorhandler": "*", + "express": "~4.0", + "extend": "*", + "method-override": "*", + "morgan": "*", + "passport": "^0.2.1", + "passport-openidconnect": "0.0.1" + } +} diff --git a/examples/openid-connect-example.js b/examples/example-using-sessions.js similarity index 99% rename from examples/openid-connect-example.js rename to examples/example-using-sessions.js index c752b0b..1905a4c 100644 --- a/examples/openid-connect-example.js +++ b/examples/example-using-sessions.js @@ -39,7 +39,11 @@ 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()); +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!!!'})); diff --git a/examples/example-using-tokens.js b/examples/example-using-tokens.js new file mode 100644 index 0000000..1905a4c --- /dev/null +++ b/examples/example-using-tokens.js @@ -0,0 +1,511 @@ + +/** + * Module dependencies. + */ + +var crypto = require('crypto'), + express = require('express'), + expressSession = require('express-session'), + http = require('http'), + path = require('path'), + querystring = require('querystring'), + rs = require('connect-redis')(expressSession), + extend = require('extend'), + test = { + status: 'new' + }, + logger = require('morgan'), + bodyParser = require('body-parser'), + cookieParser = require('cookie-parser'), + errorHandler = require('errorhandler'), + methodOverride = require('method-override'); + +var app = express(); + +var options = { + login_url: '/my/login', + consent_url: '/user/consent', + scopes: { + 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;}}}}, + 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(app.router); + +//redirect to login +app.get('/', function(req, res) { + res.redirect('/my/login'); +}); + +//Login form (I use email as user name) +app.get('/my/login', function(req, res, next) { + var head = 'Login'; + var inputs = ''; + var error = req.session.error?'
'+req.session.error+'
':''; + var body = '

Login

'+inputs+'
'+error; + res.send(''+head+body+''); +}); + +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); + } + }); +}; + +var afterLogin = function (req, res, next) { + res.redirect(req.param('return_url')||'/user'); +}; + +var loginError = function (err, req, res, next) { + 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'); +}); + +//authorization endpoint +app.get('/user/authorize', oidc.auth()); + +//token endpoint +app.post('/user/token', oidc.token()); + +//user consent form +app.get('/user/consent', function(req, res, next) { + var head = 'Consent'; + var lis = []; + for(var i in req.session.scopes) { + lis.push('
  • '+i+': '+req.session.scopes[i].explain+'
  • '); + } + var ul = ''; + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Consent

    '+ul+'
    '+error; + res.send(''+head+body+''); +}); + +//process user consent form +app.post('/user/consent', oidc.consent()); + +//user creation form +app.get('/user/create', function(req, res, next) { + 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' + } + }; + for(var i in fields) { + inputs += '
    '; + } + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Sign in

    '+inputs+'
    '+error; + res.send(''+head+body+''); +}); + +//process user creation +app.post('/user/create', oidc.use({policies: {loggedIn: false}, models: 'user'}), function(req, res, next) { + 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) { + 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'); + } + }); + } + }); +}); + +app.get('/user', oidc.check(), function(req, res, next){ + 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/bar', oidc.check('bar'), function(req, res, next){ + res.send('

    Page restricted by 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/or', oidc.check(/bar|foo/), function(req, res, next){ + res.send('

    Page restricted by "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) { + inputs += '
    '+fields[i].html+'
    '; + } + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Register Client

    '+inputs+'
    '+error; + res.send(''+head+body+''); + } else if(!err) { + mkId(); + } else { + next(err); + } + }); + }; + mkId(); +}); + +//process client register +app.post('/client/register', oidc.use('client'), function(req, res, next) { + 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){ + if(!err && client) { + res.redirect('/client/'+client.id); + } else { + next(err); + } + }); +}); + +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/: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', 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 = ''; + 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('')+'
    '); + }); + 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; + 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:
    "; + var 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 '+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+'
    '); + } + }); + }); + + // 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); + } +}); + + + +// development only +if ('development' == app.get('env')) { + app.use(errorHandler()); +} + +function mkFields(params) { + 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; + } + } + } + return fields; +} + + 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')); +}); From cfb2fe8b65dc34fd2db639ca762aaed88d4b6945 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Mon, 2 Mar 2015 08:41:55 -0800 Subject: [PATCH 02/13] Work on examples --- .jscsrc | 36 ++ .jshintrc | 18 + examples/backend-only/client.js | 22 + examples/backend-only/server.js | 521 ++++++++++++++++++ .../authorization-code-grant-with-sessions.js | 145 +++++ examples/client/authorization-code-grant.js | 113 ++++ examples/client/example-app.js | 39 +- examples/example-using-tokens.js | 15 +- 8 files changed, 894 insertions(+), 15 deletions(-) create mode 100644 .jscsrc create mode 100644 .jshintrc create mode 100644 examples/backend-only/client.js create mode 100644 examples/backend-only/server.js create mode 100644 examples/client/authorization-code-grant-with-sessions.js create mode 100644 examples/client/authorization-code-grant.js diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..ff3a8ab --- /dev/null +++ b/.jscsrc @@ -0,0 +1,36 @@ +{ + "disallowEmptyBlocks": true, + "disallowKeywords": ["with"], + "disallowMixedSpacesAndTabs": true, + "disallowMultipleLineStrings": true, + "disallowQuotedKeysInObjects": "allButReserved", + "disallowSpaceAfterBinaryOperators": ["+", "-", "/", "*"], + "disallowSpaceAfterLineComment": true, + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforeBinaryOperators": [","], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "disallowSpacesInAnonymousFunctionExpression": { "beforeOpeningRoundBrace": true }, + "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideParentheses": true, + "disallowTrailingComma": true, + "disallowTrailingWhitespace": true, + "requireCapitalizedConstructors": true, + "requireCommaBeforeLineBreak": true, + "requireDotNotation": true, + "requireLineFeedAtFileEnd": true, + "requireSpaceAfterBinaryOperators": ["=", "==", "===", "!=", "!==", ">", "<", ">=", "<="], + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", "<", ">=", "<="], + "requireSpaceBetweenArguments": true, + "requireSpacesInAnonymousFunctionExpression": { "beforeOpeningCurlyBrace": true }, + "requireSpacesInFunctionDeclaration": { "beforeOpeningCurlyBrace": true }, + "requireSpacesInFunctionExpression": { "beforeOpeningCurlyBrace": true }, + "requireSpacesInNamedFunctionExpression": { "beforeOpeningCurlyBrace": true }, + "requireSpacesInConditionalExpression": true, + "requireSpacesInForStatement": true, + "validateIndentation": 2, + "validateLineBreaks": "LF", + "validateQuoteMarks": "'" +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..f1149ee --- /dev/null +++ b/.jshintrc @@ -0,0 +1,18 @@ +{ + "node": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "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/backend-only/client.js b/examples/backend-only/client.js new file mode 100644 index 0000000..6ef8517 --- /dev/null +++ b/examples/backend-only/client.js @@ -0,0 +1,22 @@ + +/** + * Module dependencies. + */ + + +var crypto = require('crypto'), + express = require('express'), + expressSession = require('express-session'), + http = require('http'), + path = require('path'), + querystring = require('querystring'), + rs = require('connect-redis')(expressSession), + extend = require('extend'), + test = { + status: 'new' + }, + logger = require('morgan'), + bodyParser = require('body-parser'), + cookieParser = require('cookie-parser'), + errorHandler = require('errorhandler'), + methodOverride = require('method-override'); diff --git a/examples/backend-only/server.js b/examples/backend-only/server.js new file mode 100644 index 0000000..cf466ce --- /dev/null +++ b/examples/backend-only/server.js @@ -0,0 +1,521 @@ + +/** + * Module dependencies. + */ + +var crypto = require('crypto'), + express = require('express'), + expressSession = require('express-session'), + http = require('http'), + path = require('path'), + querystring = require('querystring'), + Redis = require('connect-redis')(expressSession), + extend = require('extend'), + + test = { + status: 'new' + }, + logger = require('morgan'), + bodyParser = require('body-parser'), + cookieParser = require('cookie-parser'), + errorHandler = require('errorhandler'), + methodOverride = require('method-override'); + +var app = express(); + +var options = { + login_url: '/my/login', + consent_url: '/user/consent', + scopes: { + 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;}}}}, + app: app +}; +var oidc = require('../../index').oidc(options); + +//all environments +app.set('port', process.env.PORT || 3001); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ + extended: true +})); +app.use(methodOverride()); +app.use(cookieParser('Some Secret!!!')); +app.use(expressSession({store: new Redis({host: '127.0.0.1', port: 6379}), secret: 'Some Secret!!!'})); +//app.use(app.router); + +//redirect to login +app.get('/', function(req, res) { + res.redirect('/my/login'); +}); + +//Login form (I use email as user name) +app.get('/my/login', function(req, res, next) { + var head = 'Login'; + var inputs = ''; + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Login

    '+inputs+'
    '+error; + res.send(''+head+body+''); +}); + +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); + } + }); +}; + +var afterLogin = function (req, res, next) { + res.redirect(req.param('return_url')||'/user'); +}; + +var loginError = function (err, req, res, next) { + 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'); +}); + +//authorization endpoint +app.get('/user/authorize', oidc.auth()); + +//token endpoint +app.post('/user/token', + /* + function(req, res, next) { + console.log('req'); + console.log(req); + console.log('res'); + console.log(res); + next(); + }, + //*/ + oidc.token()); + +//user consent form +app.get('/user/consent', function(req, res, next) { + var head = 'Consent'; + var lis = []; + for(var i in req.session.scopes) { + lis.push('
  • '+i+': '+req.session.scopes[i].explain+'
  • '); + } + var ul = '
      '+lis.join('')+'
    '; + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Consent

    '+ul+'
    '+error; + res.send(''+head+body+''); +}); + +//process user consent form +app.post('/user/consent', oidc.consent()); + +//user creation form +app.get('/user/create', function(req, res, next) { + 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' + } + }; + for(var i in fields) { + inputs += '
    '; + } + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Sign in

    '+inputs+'
    '+error; + res.send(''+head+body+''); +}); + +//process user creation +app.post('/user/create', oidc.use({policies: {loggedIn: false}, models: 'user'}), function(req, res, next) { + 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) { + res.redirect(req.path); + } else { + req.body.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'); + } + }); + } + }); +}); + +app.get('/user', oidc.check(), function(req, res, next){ + 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/bar', oidc.check('bar'), function(req, res, next){ + res.send('

    Page restricted by 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/or', oidc.check(/bar|foo/), function(req, res, next){ + res.send('

    Page restricted by "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) { + inputs += '
    '+fields[i].html+'
    '; + } + var error = req.session.error?'
    '+req.session.error+'
    ':''; + var body = '

    Register Client

    '+inputs+'
    '+error; + res.send(''+head+body+''); + } else if(!err) { + mkId(); + } else { + next(err); + } + }); + }; + mkId(); +}); + +//process client register +app.post('/client/register', oidc.use('client'), function(req, res, next) { + 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){ + if(!err && client) { + res.redirect('/client/'+client.id); + } else { + next(err); + } + }); +}); + +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/: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
    • Key: '+client.key+'
    • Secret: '+client.secret+'
    • Redirect Uris:
        '; + client.redirect_uris.forEach(function(uri){ + html += '
      • '+uri+'
      • '; + }); + html+='
    '; + 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', 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 = ''; + 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('')+'
    '); + }); + 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; + 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:
    "; + var 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 '+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+'
    '); + } + }); + }); + + // 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); + } +}); + + + +// development only +if ('development' == app.get('env')) { + app.use(errorHandler()); +} + +function mkFields(params) { + 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; + } + } + } + return fields; +} + + 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')); +}); diff --git a/examples/client/authorization-code-grant-with-sessions.js b/examples/client/authorization-code-grant-with-sessions.js new file mode 100644 index 0000000..8648359 --- /dev/null +++ b/examples/client/authorization-code-grant-with-sessions.js @@ -0,0 +1,145 @@ +/** + * Module dependencies. + */ + +var bodyParser = require('body-parser'); +var express = require('express'); +var expressSession = require('express-session'); +var http = require('http'); +var logger = require('morgan'); +var passport = require('passport'); +var openid = require('passport-openidconnect'); + +var app = module.exports = express(); + +passport.use(new openid.Strategy({ + authorizationURL: 'http://localhost:3001/user/authorize', + tokenURL: 'http://localhost:3001/user/token', + clientID: '7a956c6a0e62f4b961d73b88de501fee', + clientSecret: '4d74027532ceba778aa280d5f620f152', + callbackURL: 'http://localhost:3000/users', + userInfoURL: 'http://localhost:3000/users/local%40andersriutta.com' +}, +function(accessToken, refreshToken, profile, done) { + console.log('accessToken'); + console.log(accessToken); + console.log('refreshToken'); + console.log(refreshToken); + console.log('profile'); + console.log(profile); + console.log('done'); + console.log(done); + var user = { + oauthID: profile.id, + name: profile.displayName, + created: Date.now() + }; + done(null, user); +})); + +app.set('port', process.env.PORT || 3000); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({extended: false})); +app.use(expressSession({secret: 'keyboard cat'})); + +app.use(passport.initialize()); + +// Ad-hoc example resource method + +app.resource = function(path, obj) { + this.get(path, + passport.authenticate('openidconnect', {}), + obj.index); + this.get(path + '/:a..:b.:format?', function(req, res){ + var a = parseInt(req.params.a, 10); + var b = parseInt(req.params.b, 10); + var format = req.params.format; + obj.range(req, res, a, b, format); + }); + this.get(path + '/:id', obj.show); + this.delete(path + '/:id', + function(req, res){ + var id = parseInt(req.params.id, 10); + obj.destroy(req, res, id); + }); +}; + +// Fake records + +var users = [ + { name: 'tj' } + , { name: 'ciaran' } + , { name: 'aaron' } + , { name: 'guillermo' } + , { name: 'simon' } + , { name: 'tobi' } +]; + +// Fake controller. + +var User = { + index: function(req, res){ + res.send(users); + }, + show: function(req, res){ + res.send(users[req.params.id] || { error: 'Cannot find user' }); + }, + destroy: function(req, res, id){ + var destroyed = id in users; + delete users[id]; + res.send(destroyed ? 'destroyed' : 'Cannot find user'); + }, + range: function(req, res, a, b, format){ + var range = users.slice(a, b + 1); + switch (format) { + case 'json': + res.send(range); + break; + case 'html': + default: + var html = '
      ' + range.map(function(user){ + return '
    • ' + user.name + '
    • '; + }).join('\n') + '
    '; + res.send(html); + break; + } + } +}; + +// curl http://localhost:3000/users -- responds with all users +// curl http://localhost:3000/users/1 -- responds with user 1 +// curl http://localhost:3000/users/4 -- responds with error +// curl http://localhost:3000/users/1..3 -- responds with several users +// curl -X DELETE http://localhost:3000/users/1 -- deletes the user + +app.resource('/users', + User); + +app.get('/', + passport.authenticate('openidconnect', {}), + function(req, res){ + res.send([ + '

    Examples:

      ' + , '
    • GET /users
    • ' + , '
    • GET /users/1
    • ' + , '
    • GET /users/3
    • ' + , '
    • GET /users/1..3
    • ' + , '
    • GET /users/1..3.json
    • ' + , '
    • DELETE /users/4
    • ' + , '
    ' + ].join('\n')); + }); + +/* istanbul ignore next */ +/* +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} +//*/ + +http.createServer(app).listen(app.get('port'), function(){ + console.log('Express server listening on port ' + app.get('port')); +}); + diff --git a/examples/client/authorization-code-grant.js b/examples/client/authorization-code-grant.js new file mode 100644 index 0000000..5b974fc --- /dev/null +++ b/examples/client/authorization-code-grant.js @@ -0,0 +1,113 @@ +/** + * Module dependencies. + */ + +var express = require('express'); + +var app = module.exports = express(); + +// create an error with .status. we +// can then use the property in our +// custom error handler (Connect repects this prop as well) + +function error(status, msg) { + var err = new Error(msg); + err.status = status; + return err; +} + +// if we wanted to supply more than JSON, we could +// use something similar to the content-negotiation +// example. + +// here we validate the API key, +// by mounting this middleware to /api +// meaning only paths prefixed with "/api" +// will cause this middleware to be invoked + +app.use('/api', function(req, res, next){ + var key = req.query['api-key']; + + // key isn't present + if (!key) return next(error(400, 'api key required')); + + // key is invalid + if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key')); + + // all good, store req.key for route access + req.key = key; + next(); +}); + +// map of valid api keys, typically mapped to +// account info with some sort of database like redis. +// api keys do _not_ serve as authentication, merely to +// track API usage or help prevent malicious behavior etc. + +var apiKeys = ['foo', 'bar', 'baz']; + +// these two objects will serve as our faux database + +var repos = [ + { name: 'express', url: 'http://github.com/strongloop/express' } + , { name: 'stylus', url: 'http://github.com/learnboost/stylus' } + , { name: 'cluster', url: 'http://github.com/learnboost/cluster' } +]; + +var users = [ + { name: 'tobi' } + , { name: 'loki' } + , { name: 'jane' } +]; + +var userRepos = { + tobi: [repos[0], repos[1]] + , loki: [repos[1]] + , jane: [repos[2]] +}; + +// we now can assume the api key is valid, +// and simply expose the data + +// test with curl -i -H "Accept: application/json" localhost:3000/api/users?api-key=foo +app.get('/api/users', function(req, res, next){ + res.send(users); +}); + +app.get('/api/repos', function(req, res, next){ + res.send(repos); +}); + +app.get('/api/user/:name/repos', function(req, res, next){ + var name = req.params.name; + var user = userRepos[name]; + + if (user) res.send(user); + else next(); +}); + +// middleware with an arity of 4 are considered +// error handling middleware. When you next(err) +// it will be passed through the defined middleware +// in order, but ONLY those with an arity of 4, ignoring +// regular middleware. +app.use(function(err, req, res, next){ + // whatever you want here, feel free to populate + // properties on `err` to treat it differently in here. + res.status(err.status || 500); + res.send({ error: err.message }); +}); + +// our custom JSON 404 middleware. Since it's placed last +// it will be the last middleware called, if all others +// invoke next() and do not respond. +app.use(function(req, res){ + res.status(404); + res.send({ error: "Lame, can't find that" }); +}); + +/* istanbul ignore next */ +if (!module.parent) { + app.listen(3000); + console.log('Express started on port 3000'); +} diff --git a/examples/client/example-app.js b/examples/client/example-app.js index 51f9776..646a943 100644 --- a/examples/client/example-app.js +++ b/examples/client/example-app.js @@ -27,10 +27,10 @@ passport.use(new LocalStrategy( passport.use(new openid.Strategy({ authorizationURL: 'http://localhost:3001/user/authorize', tokenURL: 'http://localhost:3001/user/token', - clientID: '7a956c6a0e62f4b961d73b88de501fee', - clientSecret: '4d74027532ceba778aa280d5f620f152', + clientID: '73e7ea98700270fd98f1ed168838e2ec', + clientSecret: 'ab80faca8ed14b4d20d7c8e6fabee5de', callbackURL: 'http://localhost:3000/users', - userInfoURL: 'http://localhost:3000/users/1' + userInfoURL: 'http://localhost:3000/users/3' }, function(accessToken, refreshToken, profile, done) { console.log('accessToken'); @@ -40,13 +40,13 @@ function(accessToken, refreshToken, profile, done) { console.log('profile'); console.log(profile); console.log('done'); - console.log(done); + console.log(done.toString()); var loggedinUser = { oauthID: profile.id, name: profile.displayName, created: Date.now() }; - done(null, loggedinUser); + done(null, loggedinUser, {boss: 'fleek'}); /* User.findOne({oauthID: profile.id}, function(err, user) { if(err) { console.log(err); } @@ -122,12 +122,25 @@ app.resource = function(path, obj) { // Fake records var users = [ - { name: 'tj' } - , { name: 'ciaran' } - , { name: 'aaron' } - , { name: 'guillermo' } - , { name: 'simon' } - , { name: 'tobi' } + { + email: 'tj@example.org', + name: 'tj' + }, { + email: 'ciaran@example.org', + name: 'ciaran' + }, { + email: 'aaron@example.org', + name: 'aaron' + }, { + email: 'guillermo@example.org', + name: 'guillermo' + }, { + email: 'simon@example.org', + name: 'simon' + }, { + email: 'tobi@example.org', + name: 'tobi' + } ]; // Fake controller. @@ -137,7 +150,9 @@ var User = { res.send(users); }, show: function(req, res){ - res.send(users[req.params.id] || { error: 'Cannot find user' }); + var user = users[req.params.id] || { error: 'Cannot find user' }; + user.sub = req.params.id; + res.send(user); }, destroy: function(req, res, id){ var destroyed = id in users; diff --git a/examples/example-using-tokens.js b/examples/example-using-tokens.js index 1905a4c..c970df2 100644 --- a/examples/example-using-tokens.js +++ b/examples/example-using-tokens.js @@ -39,7 +39,6 @@ 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 @@ -96,7 +95,17 @@ app.all('/logout', oidc.removetokens(), function(req, res, next) { app.get('/user/authorize', oidc.auth()); //token endpoint -app.post('/user/token', oidc.token()); +app.post('/user/token', + /* + function(req, res, next) { + console.log('req'); + console.log(req); + console.log('res'); + console.log(res); + next(); + }, + //*/ + oidc.token()); //user consent form app.get('/user/consent', function(req, res, next) { @@ -165,7 +174,7 @@ app.post('/user/create', oidc.use({policies: {loggedIn: false}, models: 'user'}) 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.body.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.'; From 600d7a04b8f0f0aca016b83cb5e160ce9ef012ff Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Mon, 9 Mar 2015 10:05:20 -0700 Subject: [PATCH 03/13] Added. --- .jscsrc | 110 ++++++++++++++++++++++--------- examples/example-using-tokens.js | 1 - 2 files changed, 78 insertions(+), 33 deletions(-) diff --git a/.jscsrc b/.jscsrc index ff3a8ab..03b41f9 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,36 +1,82 @@ { - "disallowEmptyBlocks": true, - "disallowKeywords": ["with"], - "disallowMixedSpacesAndTabs": true, - "disallowMultipleLineStrings": true, - "disallowQuotedKeysInObjects": "allButReserved", - "disallowSpaceAfterBinaryOperators": ["+", "-", "/", "*"], - "disallowSpaceAfterLineComment": true, - "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], - "disallowSpaceBeforeBinaryOperators": [","], - "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], - "disallowSpacesInAnonymousFunctionExpression": { "beforeOpeningRoundBrace": true }, - "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideObjectBrackets": true, - "disallowSpacesInsideParentheses": true, - "disallowTrailingComma": true, - "disallowTrailingWhitespace": true, - "requireCapitalizedConstructors": true, - "requireCommaBeforeLineBreak": true, - "requireDotNotation": true, - "requireLineFeedAtFileEnd": true, - "requireSpaceAfterBinaryOperators": ["=", "==", "===", "!=", "!==", ">", "<", ">=", "<="], - "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], - "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", "<", ">=", "<="], - "requireSpaceBetweenArguments": true, - "requireSpacesInAnonymousFunctionExpression": { "beforeOpeningCurlyBrace": true }, - "requireSpacesInFunctionDeclaration": { "beforeOpeningCurlyBrace": true }, - "requireSpacesInFunctionExpression": { "beforeOpeningCurlyBrace": true }, - "requireSpacesInNamedFunctionExpression": { "beforeOpeningCurlyBrace": true }, - "requireSpacesInConditionalExpression": true, + "requireCurlyBraces": [ + "while", + "do", + "try", + "catch" + ], + "requireSpaceAfterKeywords": [ + "else", + "while", + "do", + "return", + "try" + ], + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInsideArrayBrackets": "all", + "requireSpaceBeforeBlockStatements": true, "requireSpacesInForStatement": true, - "validateIndentation": 2, + "disallowKeywords": [ + "with" + ], + "requireLineFeedAtFileEnd": true, "validateLineBreaks": "LF", - "validateQuoteMarks": "'" + "validateIndentation": 4, + "requireSpaceAfterPrefixUnaryOperators": [ + "++", + "--" + ], + "requireSpaceBetweenArguments": true, + "requireSpaceBeforePostfixUnaryOperators": [ + "--" + ], + "requireSpaceBeforeBinaryOperators": [ + "-", + "=", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<=" + ], + "requireSpaceAfterBinaryOperators": [ + "-", + "=", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<=" + ], + "disallowSpaceBeforeBinaryOperators": [ + ",", + "+", + "/", + "*", + "<" + ], + "disallowSpaceAfterKeywords": [ + "if", + "for", + "switch", + "catch" + ], + "disallowSpacesInConditionalExpression": { + "beforeAlternate": true + }, + "disallowSpaceAfterBinaryOperators": [ + "+", + "/", + "*", + "<" + ], + "disallowSpaceBeforePostfixUnaryOperators": [ + "++" + ] } diff --git a/examples/example-using-tokens.js b/examples/example-using-tokens.js index c970df2..cffc1ea 100644 --- a/examples/example-using-tokens.js +++ b/examples/example-using-tokens.js @@ -35,7 +35,6 @@ var options = { }; var oidc = require('../index').oidc(options); - // all environments app.set('port', process.env.PORT || 3001); app.use(logger('dev')); From 91aace26ab0b2eac24b14bd668a2d866a431251d Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sat, 21 Mar 2015 21:51:26 -0700 Subject: [PATCH 04/13] Linted code. --- .jscsrc | 7 +- .jshintrc | 4 +- index.js | 374 +++++++++++++++++++++++++++++------------------------- 3 files changed, 207 insertions(+), 178 deletions(-) diff --git a/.jscsrc b/.jscsrc index 03b41f9..6f4c8da 100644 --- a/.jscsrc +++ b/.jscsrc @@ -15,7 +15,6 @@ "requireSpacesInFunctionExpression": { "beforeOpeningCurlyBrace": true }, - "requireSpacesInsideArrayBrackets": "all", "requireSpaceBeforeBlockStatements": true, "requireSpacesInForStatement": true, "disallowKeywords": [ @@ -23,7 +22,6 @@ ], "requireLineFeedAtFileEnd": true, "validateLineBreaks": "LF", - "validateIndentation": 4, "requireSpaceAfterPrefixUnaryOperators": [ "++", "--" @@ -55,12 +53,13 @@ "<=" ], "disallowSpaceBeforeBinaryOperators": [ - ",", "+", "/", "*", "<" ], + "disallowPaddingNewlinesInBlocks": true, + "validateIndentation": 4, "disallowSpaceAfterKeywords": [ "if", "for", @@ -76,6 +75,8 @@ "*", "<" ], + "disallowSpacesInsideObjectBrackets": "all", + "disallowSpacesInsideArrayBrackets": "all", "disallowSpaceBeforePostfixUnaryOperators": [ "++" ] diff --git a/.jshintrc b/.jshintrc index f1149ee..442455e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,8 +1,8 @@ { "node": true, "bitwise": true, - "camelcase": true, - "curly": true, + "camelcase": false, + "curly": false, "forin": true, "immed": true, "latedef": true, diff --git a/index.js b/index.js index b41bdc1..dd2059c 100644 --- a/index.js +++ b/index.js @@ -16,13 +16,12 @@ crypto = require('crypto'), _ = require('lodash'), extend = require('extend'), url = require('url'), -Q = require('q'), +promiseQ = 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', @@ -79,7 +78,7 @@ var defaults = { beforeCreate: function(values, next) { if(values.password) { if(values.password != values.passConfirm) { - return next("Password and confirmation does not match"); + return next('Password and confirmation does not match'); } var sha256 = crypto.createHash('sha256'); sha256.update(values.password); @@ -90,7 +89,7 @@ var defaults = { beforeUpdate: function(values, next) { if(values.password) { if(values.password != values.passConfirm) { - return next("Password and confirmation does not match"); + return next('Password and confirmation does not match'); } var sha256 = crypto.createHash('sha256'); sha256.update(values.password); @@ -114,14 +113,13 @@ var defaults = { credentialsFlow: {type: 'boolean', defaultsTo: false} }, beforeCreate: function(values, next) { + var sha256 = crypto.createHash('sha256'); 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()+''); @@ -206,7 +204,7 @@ function parse_authorization(authorization) { return null; var username = creds.slice(0, i); - password = creds.slice(i + 1); + var password = creds.slice(i+1); return [username, password]; } @@ -218,22 +216,27 @@ function OpenIDConnect(options) { cleanObj(this.settings.models, true); for(var i in this.settings.policies) { - this.settings.policies[i] = this.settings.policies[i].bind(this); + 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]; + 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]); + 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, @@ -252,7 +255,7 @@ OpenIDConnect.prototype.done = function() { OpenIDConnect.prototype.model = function(name) { return this.orm.model(name); -} +}; OpenIDConnect.prototype.use = function(name) { var alien = {}; @@ -284,7 +287,7 @@ OpenIDConnect.prototype.use = function(name) { OpenIDConnect.prototype.getOrm = function() { return this.orm; -} +}; /*OpenIDConnect.prototype.getClientParams = function() { return this.orm.client.getParams(); };*/ @@ -307,7 +310,7 @@ OpenIDConnect.prototype.searchUser = function(parts, callback) { OpenIDConnect.prototype.errorHandle = function(res, uri, error, desc) { if(uri) { - var redirect = url.parse(uri,true); + 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)); @@ -323,47 +326,54 @@ OpenIDConnect.prototype.endpointParams = function (spec, req, res, 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; + 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[i2])) { + error = spec[i2](params); } - } else if (_.isFunction(spec[i])) { - error = spec[i](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; + } } } } @@ -399,7 +409,7 @@ OpenIDConnect.prototype.login = function(validateUser) { delete req.session.user; } if(user.sub) { - if(typeof user.sub ==='function') { + if(typeof user.sub === 'function') { req.session.sub = user.sub(); } else { req.session.sub = user.sub; @@ -412,7 +422,7 @@ OpenIDConnect.prototype.login = function(validateUser) { return next(error); } }); - }]; + }]; }; /** @@ -433,8 +443,8 @@ OpenIDConnect.prototype.auth = function() { scope: true, redirect_uri: true, state: false, - nonce: function(params){ - return params.response_type.indexOf('id_token')!==-1; + nonce: function(params) { + return params.response_type.indexOf('id_token') !== -1; }, display: false, prompt: false, @@ -450,11 +460,12 @@ OpenIDConnect.prototype.auth = function() { self.endpointParams(spec, req, res, next); }, self.use(['client', 'consent', 'auth', 'access']), + /*jshint unused:false */ function(req, res, next) { - Q(req.parsedParams).then(function(params) { + promiseQ(req.parsedParams).then(function(params) { //Step 2: Check if response_type is supported and client_id is valid. - var deferred = Q.defer(); + var deferred = promiseQ.defer(); switch(params.response_type) { case 'none': case 'code': @@ -481,67 +492,68 @@ OpenIDConnect.prototype.auth = function() { }); return deferred.promise; - }).then(function(params){ + }).then(function(params) { //Step 3: Check if scopes are valid, and if consent was given. - var deferred = Q.defer(); + var deferred = promiseQ.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); - }); + reqsco.forEach(function(scope) { + var innerDef = promiseQ.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 if(util.isRegExp(scope)) { + var inS = false; + access.scope.forEach(function(s) { + if(scope.test(s)) { + inS = true; + } + }); + // TODO this line is confusing. What is it doing? + !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 { + req.check = req.check||{}; + req.check.scopes = access.scope; + next(); + } } else { self.errorHandle(res, null, 'unauthorized_client', 'Access token is not valid.'); } @@ -1081,6 +1106,8 @@ OpenIDConnect.prototype.userInfo = function() { return [ self.check('openid', /profile|email/), self.use('user'), + + /*jshint unused:false */ function(req, res, next) { req.model.user.findOne({id: req.session.user}, function(err, user) { if(req.check.scopes.indexOf('profile') != -1) { @@ -1122,7 +1149,8 @@ OpenIDConnect.prototype.removetokens = function() { var params = req.parsedParams; if(!params.access_token) { - params.access_token = (req.headers['authorization'] || '').indexOf('Bearer ') === 0 ? req.headers['authorization'].replace('Bearer', '').trim() : false; + 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 @@ -1134,21 +1162,21 @@ OpenIDConnect.prototype.removetokens = function() { .populate('refreshTokens') .exec(function(err, auth) { if(!err && auth) { - auth.accessTokens.forEach(function(access){ + auth.accessTokens.forEach(function(access) { access.destroy(); }); - auth.refreshTokens.forEach(function(refresh){ + auth.refreshTokens.forEach(function(refresh) { refresh.destroy(); }); auth.destroy(); - }; + } req.model.access.find({user:access.user}) - .exec(function(err,accesses){ + .exec(function(err,accesses) { if(!err && accesses) { accesses.forEach(function(access) { access.destroy(); }); - }; + } return next(); }); }); @@ -1169,4 +1197,4 @@ exports.oidc = function(options) { exports.defaults = function() { return defaults; -} +}; From 231324ec36d0fb7f3d12db95290dbcda7aa520f6 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sat, 21 Mar 2015 21:55:53 -0700 Subject: [PATCH 05/13] Go back to using provided example --- examples/README.md | 27 - examples/backend-only/client.js | 22 - examples/backend-only/server.js | 521 ------------------ .../authorization-code-grant-with-sessions.js | 145 ----- examples/client/authorization-code-grant.js | 113 ---- examples/client/example-app.js | 213 ------- examples/client/example-client.js | 113 ---- examples/client/package.json | 18 - examples/example-using-tokens.js | 519 ----------------- ...-sessions.js => openid-connect-example.js} | 6 +- 10 files changed, 1 insertion(+), 1696 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/backend-only/client.js delete mode 100644 examples/backend-only/server.js delete mode 100644 examples/client/authorization-code-grant-with-sessions.js delete mode 100644 examples/client/authorization-code-grant.js delete mode 100644 examples/client/example-app.js delete mode 100644 examples/client/example-client.js delete mode 100644 examples/client/package.json delete mode 100644 examples/example-using-tokens.js rename examples/{example-using-sessions.js => openid-connect-example.js} (99%) diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 304cf0e..0000000 --- a/examples/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# 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 by navigating to http://localhost:3001/user/create diff --git a/examples/backend-only/client.js b/examples/backend-only/client.js deleted file mode 100644 index 6ef8517..0000000 --- a/examples/backend-only/client.js +++ /dev/null @@ -1,22 +0,0 @@ - -/** - * Module dependencies. - */ - - -var crypto = require('crypto'), - express = require('express'), - expressSession = require('express-session'), - http = require('http'), - path = require('path'), - querystring = require('querystring'), - rs = require('connect-redis')(expressSession), - extend = require('extend'), - test = { - status: 'new' - }, - logger = require('morgan'), - bodyParser = require('body-parser'), - cookieParser = require('cookie-parser'), - errorHandler = require('errorhandler'), - methodOverride = require('method-override'); diff --git a/examples/backend-only/server.js b/examples/backend-only/server.js deleted file mode 100644 index cf466ce..0000000 --- a/examples/backend-only/server.js +++ /dev/null @@ -1,521 +0,0 @@ - -/** - * Module dependencies. - */ - -var crypto = require('crypto'), - express = require('express'), - expressSession = require('express-session'), - http = require('http'), - path = require('path'), - querystring = require('querystring'), - Redis = require('connect-redis')(expressSession), - extend = require('extend'), - - test = { - status: 'new' - }, - logger = require('morgan'), - bodyParser = require('body-parser'), - cookieParser = require('cookie-parser'), - errorHandler = require('errorhandler'), - methodOverride = require('method-override'); - -var app = express(); - -var options = { - login_url: '/my/login', - consent_url: '/user/consent', - scopes: { - 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;}}}}, - app: app -}; -var oidc = require('../../index').oidc(options); - -//all environments -app.set('port', process.env.PORT || 3001); -app.use(logger('dev')); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ - extended: true -})); -app.use(methodOverride()); -app.use(cookieParser('Some Secret!!!')); -app.use(expressSession({store: new Redis({host: '127.0.0.1', port: 6379}), secret: 'Some Secret!!!'})); -//app.use(app.router); - -//redirect to login -app.get('/', function(req, res) { - res.redirect('/my/login'); -}); - -//Login form (I use email as user name) -app.get('/my/login', function(req, res, next) { - var head = 'Login'; - var inputs = ''; - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Login

    '+inputs+'
    '+error; - res.send(''+head+body+''); -}); - -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); - } - }); -}; - -var afterLogin = function (req, res, next) { - res.redirect(req.param('return_url')||'/user'); -}; - -var loginError = function (err, req, res, next) { - 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'); -}); - -//authorization endpoint -app.get('/user/authorize', oidc.auth()); - -//token endpoint -app.post('/user/token', - /* - function(req, res, next) { - console.log('req'); - console.log(req); - console.log('res'); - console.log(res); - next(); - }, - //*/ - oidc.token()); - -//user consent form -app.get('/user/consent', function(req, res, next) { - var head = 'Consent'; - var lis = []; - for(var i in req.session.scopes) { - lis.push('
  • '+i+': '+req.session.scopes[i].explain+'
  • '); - } - var ul = '
      '+lis.join('')+'
    '; - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Consent

    '+ul+'
    '+error; - res.send(''+head+body+''); -}); - -//process user consent form -app.post('/user/consent', oidc.consent()); - -//user creation form -app.get('/user/create', function(req, res, next) { - 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' - } - }; - for(var i in fields) { - inputs += '
    '; - } - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Sign in

    '+inputs+'
    '+error; - res.send(''+head+body+''); -}); - -//process user creation -app.post('/user/create', oidc.use({policies: {loggedIn: false}, models: 'user'}), function(req, res, next) { - 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) { - res.redirect(req.path); - } else { - req.body.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'); - } - }); - } - }); -}); - -app.get('/user', oidc.check(), function(req, res, next){ - res.send('

    User Page

    '); -}); - -//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/bar', oidc.check('bar'), function(req, res, next){ - res.send('

    Page restricted by 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/or', oidc.check(/bar|foo/), function(req, res, next){ - res.send('

    Page restricted by "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) { - inputs += '
    '+fields[i].html+'
    '; - } - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Register Client

    '+inputs+'
    '+error; - res.send(''+head+body+''); - } else if(!err) { - mkId(); - } else { - next(err); - } - }); - }; - mkId(); -}); - -//process client register -app.post('/client/register', oidc.use('client'), function(req, res, next) { - 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){ - if(!err && client) { - res.redirect('/client/'+client.id); - } else { - next(err); - } - }); -}); - -app.get('/client', oidc.use('client'), function(req, res, next){ - var head ='

    Clients Page

    '; - 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

    • Key: '+client.key+'
    • Secret: '+client.secret+'
    • Redirect Uris:
        '; - client.redirect_uris.forEach(function(uri){ - html += '
      • '+uri+'
      • '; - }); - html+='
    '; - res.send(html); - } else { - res.send('

    No Client Fount!

    '); - } - }); -}); - -app.get('/test/clear', function(req, res, next){ - 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 = ''; - 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('')+'
    '); - }); - 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; - 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:
    "; - var 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 '+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+'
    '); - } - }); - }); - - // 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); - } -}); - - - -// development only -if ('development' == app.get('env')) { - app.use(errorHandler()); -} - -function mkFields(params) { - 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; - } - } - } - return fields; -} - - 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')); -}); diff --git a/examples/client/authorization-code-grant-with-sessions.js b/examples/client/authorization-code-grant-with-sessions.js deleted file mode 100644 index 8648359..0000000 --- a/examples/client/authorization-code-grant-with-sessions.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Module dependencies. - */ - -var bodyParser = require('body-parser'); -var express = require('express'); -var expressSession = require('express-session'); -var http = require('http'); -var logger = require('morgan'); -var passport = require('passport'); -var openid = require('passport-openidconnect'); - -var app = module.exports = express(); - -passport.use(new openid.Strategy({ - authorizationURL: 'http://localhost:3001/user/authorize', - tokenURL: 'http://localhost:3001/user/token', - clientID: '7a956c6a0e62f4b961d73b88de501fee', - clientSecret: '4d74027532ceba778aa280d5f620f152', - callbackURL: 'http://localhost:3000/users', - userInfoURL: 'http://localhost:3000/users/local%40andersriutta.com' -}, -function(accessToken, refreshToken, profile, done) { - console.log('accessToken'); - console.log(accessToken); - console.log('refreshToken'); - console.log(refreshToken); - console.log('profile'); - console.log(profile); - console.log('done'); - console.log(done); - var user = { - oauthID: profile.id, - name: profile.displayName, - created: Date.now() - }; - done(null, user); -})); - -app.set('port', process.env.PORT || 3000); -app.use(logger('dev')); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: false})); -app.use(expressSession({secret: 'keyboard cat'})); - -app.use(passport.initialize()); - -// Ad-hoc example resource method - -app.resource = function(path, obj) { - this.get(path, - passport.authenticate('openidconnect', {}), - obj.index); - this.get(path + '/:a..:b.:format?', function(req, res){ - var a = parseInt(req.params.a, 10); - var b = parseInt(req.params.b, 10); - var format = req.params.format; - obj.range(req, res, a, b, format); - }); - this.get(path + '/:id', obj.show); - this.delete(path + '/:id', - function(req, res){ - var id = parseInt(req.params.id, 10); - obj.destroy(req, res, id); - }); -}; - -// Fake records - -var users = [ - { name: 'tj' } - , { name: 'ciaran' } - , { name: 'aaron' } - , { name: 'guillermo' } - , { name: 'simon' } - , { name: 'tobi' } -]; - -// Fake controller. - -var User = { - index: function(req, res){ - res.send(users); - }, - show: function(req, res){ - res.send(users[req.params.id] || { error: 'Cannot find user' }); - }, - destroy: function(req, res, id){ - var destroyed = id in users; - delete users[id]; - res.send(destroyed ? 'destroyed' : 'Cannot find user'); - }, - range: function(req, res, a, b, format){ - var range = users.slice(a, b + 1); - switch (format) { - case 'json': - res.send(range); - break; - case 'html': - default: - var html = '
      ' + range.map(function(user){ - return '
    • ' + user.name + '
    • '; - }).join('\n') + '
    '; - res.send(html); - break; - } - } -}; - -// curl http://localhost:3000/users -- responds with all users -// curl http://localhost:3000/users/1 -- responds with user 1 -// curl http://localhost:3000/users/4 -- responds with error -// curl http://localhost:3000/users/1..3 -- responds with several users -// curl -X DELETE http://localhost:3000/users/1 -- deletes the user - -app.resource('/users', - User); - -app.get('/', - passport.authenticate('openidconnect', {}), - function(req, res){ - res.send([ - '

    Examples:

      ' - , '
    • GET /users
    • ' - , '
    • GET /users/1
    • ' - , '
    • GET /users/3
    • ' - , '
    • GET /users/1..3
    • ' - , '
    • GET /users/1..3.json
    • ' - , '
    • DELETE /users/4
    • ' - , '
    ' - ].join('\n')); - }); - -/* istanbul ignore next */ -/* -if (!module.parent) { - app.listen(3000); - console.log('Express started on port 3000'); -} -//*/ - -http.createServer(app).listen(app.get('port'), function(){ - console.log('Express server listening on port ' + app.get('port')); -}); - diff --git a/examples/client/authorization-code-grant.js b/examples/client/authorization-code-grant.js deleted file mode 100644 index 5b974fc..0000000 --- a/examples/client/authorization-code-grant.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Module dependencies. - */ - -var express = require('express'); - -var app = module.exports = express(); - -// create an error with .status. we -// can then use the property in our -// custom error handler (Connect repects this prop as well) - -function error(status, msg) { - var err = new Error(msg); - err.status = status; - return err; -} - -// if we wanted to supply more than JSON, we could -// use something similar to the content-negotiation -// example. - -// here we validate the API key, -// by mounting this middleware to /api -// meaning only paths prefixed with "/api" -// will cause this middleware to be invoked - -app.use('/api', function(req, res, next){ - var key = req.query['api-key']; - - // key isn't present - if (!key) return next(error(400, 'api key required')); - - // key is invalid - if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key')); - - // all good, store req.key for route access - req.key = key; - next(); -}); - -// map of valid api keys, typically mapped to -// account info with some sort of database like redis. -// api keys do _not_ serve as authentication, merely to -// track API usage or help prevent malicious behavior etc. - -var apiKeys = ['foo', 'bar', 'baz']; - -// these two objects will serve as our faux database - -var repos = [ - { name: 'express', url: 'http://github.com/strongloop/express' } - , { name: 'stylus', url: 'http://github.com/learnboost/stylus' } - , { name: 'cluster', url: 'http://github.com/learnboost/cluster' } -]; - -var users = [ - { name: 'tobi' } - , { name: 'loki' } - , { name: 'jane' } -]; - -var userRepos = { - tobi: [repos[0], repos[1]] - , loki: [repos[1]] - , jane: [repos[2]] -}; - -// we now can assume the api key is valid, -// and simply expose the data - -// test with curl -i -H "Accept: application/json" localhost:3000/api/users?api-key=foo -app.get('/api/users', function(req, res, next){ - res.send(users); -}); - -app.get('/api/repos', function(req, res, next){ - res.send(repos); -}); - -app.get('/api/user/:name/repos', function(req, res, next){ - var name = req.params.name; - var user = userRepos[name]; - - if (user) res.send(user); - else next(); -}); - -// middleware with an arity of 4 are considered -// error handling middleware. When you next(err) -// it will be passed through the defined middleware -// in order, but ONLY those with an arity of 4, ignoring -// regular middleware. -app.use(function(err, req, res, next){ - // whatever you want here, feel free to populate - // properties on `err` to treat it differently in here. - res.status(err.status || 500); - res.send({ error: err.message }); -}); - -// our custom JSON 404 middleware. Since it's placed last -// it will be the last middleware called, if all others -// invoke next() and do not respond. -app.use(function(req, res){ - res.status(404); - res.send({ error: "Lame, can't find that" }); -}); - -/* istanbul ignore next */ -if (!module.parent) { - app.listen(3000); - console.log('Express started on port 3000'); -} diff --git a/examples/client/example-app.js b/examples/client/example-app.js deleted file mode 100644 index 646a943..0000000 --- a/examples/client/example-app.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Module dependencies. - */ - -var bodyParser = require('body-parser'); -var express = require('express'); -var http = require('http'); -var logger = require('morgan'); -var passport = require('passport'); -var openid = require('passport-openidconnect'); - -var app = module.exports = express(); - -/* -passport.use(new LocalStrategy( - function(username, password, done) { - User.findOne({ username: username }, function (err, user) { - if (err) { return done(err); } - if (!user) { return done(null, false); } - if (!user.verifyPassword(password)) { return done(null, false); } - return done(null, user); - }); - } -)); -//*/ - -passport.use(new openid.Strategy({ - authorizationURL: 'http://localhost:3001/user/authorize', - tokenURL: 'http://localhost:3001/user/token', - clientID: '73e7ea98700270fd98f1ed168838e2ec', - clientSecret: 'ab80faca8ed14b4d20d7c8e6fabee5de', - callbackURL: 'http://localhost:3000/users', - userInfoURL: 'http://localhost:3000/users/3' -}, -function(accessToken, refreshToken, profile, done) { - console.log('accessToken'); - console.log(accessToken); - console.log('refreshToken'); - console.log(refreshToken); - console.log('profile'); - console.log(profile); - console.log('done'); - console.log(done.toString()); - var loggedinUser = { - oauthID: profile.id, - name: profile.displayName, - created: Date.now() - }; - done(null, loggedinUser, {boss: 'fleek'}); - /* - User.findOne({oauthID: profile.id}, function(err, user) { - if(err) { console.log(err); } - if (!err && user != null) { - done(null, user); - } else { - var user = new User({ - oauthID: profile.id, - name: profile.displayName, - created: Date.now() - }); - user.save(function(err) { - if(err) { - console.log(err); - } else { - console.log("saving user ..."); - done(null, user); - }; - }); - }; - }); - //*/ -} -/* -function(err, result) { - console.log(err); - console.log(result); - console.log('Hello world'); - return false; -} -//*/ -)); - -app.set('port', process.env.PORT || 3000); -app.use(logger('dev')); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: false})); -//app.use(express.session({secret: 'keyboard cat'})); -app.use(passport.initialize()); - -/* -app.configure(function() { - app.use(express.static(__dirname + '/../../public')); - app.use(express.cookieParser()); - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({extended: false})); - app.use(express.session({secret: 'keyboard cat'})); - app.use(passport.initialize()); - app.use(passport.session()); -}); -//*/ - -// Ad-hoc example resource method - -app.resource = function(path, obj) { - this.get(path, - passport.authenticate('openidconnect', { session: false }), // THIS WORKS!! - obj.index); - this.get(path + '/:a..:b.:format?', function(req, res){ - var a = parseInt(req.params.a, 10); - var b = parseInt(req.params.b, 10); - var format = req.params.format; - obj.range(req, res, a, b, format); - }); - this.get(path + '/:id', obj.show); - this.delete(path + '/:id', - function(req, res){ - var id = parseInt(req.params.id, 10); - obj.destroy(req, res, id); - }); -}; - -// Fake records - -var users = [ - { - email: 'tj@example.org', - name: 'tj' - }, { - email: 'ciaran@example.org', - name: 'ciaran' - }, { - email: 'aaron@example.org', - name: 'aaron' - }, { - email: 'guillermo@example.org', - name: 'guillermo' - }, { - email: 'simon@example.org', - name: 'simon' - }, { - email: 'tobi@example.org', - name: 'tobi' - } -]; - -// Fake controller. - -var User = { - index: function(req, res){ - res.send(users); - }, - show: function(req, res){ - var user = users[req.params.id] || { error: 'Cannot find user' }; - user.sub = req.params.id; - res.send(user); - }, - destroy: function(req, res, id){ - var destroyed = id in users; - delete users[id]; - res.send(destroyed ? 'destroyed' : 'Cannot find user'); - }, - range: function(req, res, a, b, format){ - var range = users.slice(a, b + 1); - switch (format) { - case 'json': - res.send(range); - break; - case 'html': - default: - var html = '
      ' + range.map(function(user){ - return '
    • ' + user.name + '
    • '; - }).join('\n') + '
    '; - res.send(html); - break; - } - } -}; - -// curl http://localhost:3000/users -- responds with all users -// curl http://localhost:3000/users/1 -- responds with user 1 -// curl http://localhost:3000/users/4 -- responds with error -// curl http://localhost:3000/users/1..3 -- responds with several users -// curl -X DELETE http://localhost:3000/users/1 -- deletes the user - -app.resource('/users', - User); - -app.get('/', - passport.authenticate('openidconnect', { session: false }), // THIS WORKS!! - function(req, res){ - res.send([ - '

    Examples:

      ' - , '
    • GET /users
    • ' - , '
    • GET /users/1
    • ' - , '
    • GET /users/3
    • ' - , '
    • GET /users/1..3
    • ' - , '
    • GET /users/1..3.json
    • ' - , '
    • DELETE /users/4
    • ' - , '
    ' - ].join('\n')); - }); - -/* istanbul ignore next */ -/* -if (!module.parent) { - app.listen(3000); - console.log('Express started on port 3000'); -} -//*/ - -http.createServer(app).listen(app.get('port'), function(){ - console.log('Express server listening on port ' + app.get('port')); -}); diff --git a/examples/client/example-client.js b/examples/client/example-client.js deleted file mode 100644 index 5b974fc..0000000 --- a/examples/client/example-client.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Module dependencies. - */ - -var express = require('express'); - -var app = module.exports = express(); - -// create an error with .status. we -// can then use the property in our -// custom error handler (Connect repects this prop as well) - -function error(status, msg) { - var err = new Error(msg); - err.status = status; - return err; -} - -// if we wanted to supply more than JSON, we could -// use something similar to the content-negotiation -// example. - -// here we validate the API key, -// by mounting this middleware to /api -// meaning only paths prefixed with "/api" -// will cause this middleware to be invoked - -app.use('/api', function(req, res, next){ - var key = req.query['api-key']; - - // key isn't present - if (!key) return next(error(400, 'api key required')); - - // key is invalid - if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key')); - - // all good, store req.key for route access - req.key = key; - next(); -}); - -// map of valid api keys, typically mapped to -// account info with some sort of database like redis. -// api keys do _not_ serve as authentication, merely to -// track API usage or help prevent malicious behavior etc. - -var apiKeys = ['foo', 'bar', 'baz']; - -// these two objects will serve as our faux database - -var repos = [ - { name: 'express', url: 'http://github.com/strongloop/express' } - , { name: 'stylus', url: 'http://github.com/learnboost/stylus' } - , { name: 'cluster', url: 'http://github.com/learnboost/cluster' } -]; - -var users = [ - { name: 'tobi' } - , { name: 'loki' } - , { name: 'jane' } -]; - -var userRepos = { - tobi: [repos[0], repos[1]] - , loki: [repos[1]] - , jane: [repos[2]] -}; - -// we now can assume the api key is valid, -// and simply expose the data - -// test with curl -i -H "Accept: application/json" localhost:3000/api/users?api-key=foo -app.get('/api/users', function(req, res, next){ - res.send(users); -}); - -app.get('/api/repos', function(req, res, next){ - res.send(repos); -}); - -app.get('/api/user/:name/repos', function(req, res, next){ - var name = req.params.name; - var user = userRepos[name]; - - if (user) res.send(user); - else next(); -}); - -// middleware with an arity of 4 are considered -// error handling middleware. When you next(err) -// it will be passed through the defined middleware -// in order, but ONLY those with an arity of 4, ignoring -// regular middleware. -app.use(function(err, req, res, next){ - // whatever you want here, feel free to populate - // properties on `err` to treat it differently in here. - res.status(err.status || 500); - res.send({ error: err.message }); -}); - -// our custom JSON 404 middleware. Since it's placed last -// it will be the last middleware called, if all others -// invoke next() and do not respond. -app.use(function(req, res){ - res.status(404); - res.send({ error: "Lame, can't find that" }); -}); - -/* istanbul ignore next */ -if (!module.parent) { - app.listen(3000); - console.log('Express started on port 3000'); -} diff --git a/examples/client/package.json b/examples/client/package.json deleted file mode 100644 index d6e6e68..0000000 --- a/examples/client/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "openid-connect-client-example", - "version": "0.0.1", - "private": true, - "scripts": { - "start": "node example-client.js" - }, - "dependencies": { - "body-parser": "*", - "errorhandler": "*", - "express": "~4.0", - "extend": "*", - "method-override": "*", - "morgan": "*", - "passport": "^0.2.1", - "passport-openidconnect": "0.0.1" - } -} diff --git a/examples/example-using-tokens.js b/examples/example-using-tokens.js deleted file mode 100644 index cffc1ea..0000000 --- a/examples/example-using-tokens.js +++ /dev/null @@ -1,519 +0,0 @@ - -/** - * Module dependencies. - */ - -var crypto = require('crypto'), - express = require('express'), - expressSession = require('express-session'), - http = require('http'), - path = require('path'), - querystring = require('querystring'), - rs = require('connect-redis')(expressSession), - extend = require('extend'), - test = { - status: 'new' - }, - logger = require('morgan'), - bodyParser = require('body-parser'), - cookieParser = require('cookie-parser'), - errorHandler = require('errorhandler'), - methodOverride = require('method-override'); - -var app = express(); - -var options = { - login_url: '/my/login', - consent_url: '/user/consent', - scopes: { - 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;}}}}, - app: app -}; -var oidc = require('../index').oidc(options); - -// all environments -app.set('port', process.env.PORT || 3001); -app.use(logger('dev')); -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(app.router); - -//redirect to login -app.get('/', function(req, res) { - res.redirect('/my/login'); -}); - -//Login form (I use email as user name) -app.get('/my/login', function(req, res, next) { - var head = 'Login'; - var inputs = ''; - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Login

    '+inputs+'
    '+error; - res.send(''+head+body+''); -}); - -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); - } - }); -}; - -var afterLogin = function (req, res, next) { - res.redirect(req.param('return_url')||'/user'); -}; - -var loginError = function (err, req, res, next) { - 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'); -}); - -//authorization endpoint -app.get('/user/authorize', oidc.auth()); - -//token endpoint -app.post('/user/token', - /* - function(req, res, next) { - console.log('req'); - console.log(req); - console.log('res'); - console.log(res); - next(); - }, - //*/ - oidc.token()); - -//user consent form -app.get('/user/consent', function(req, res, next) { - var head = 'Consent'; - var lis = []; - for(var i in req.session.scopes) { - lis.push('
  • '+i+': '+req.session.scopes[i].explain+'
  • '); - } - var ul = '
      '+lis.join('')+'
    '; - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Consent

    '+ul+'
    '+error; - res.send(''+head+body+''); -}); - -//process user consent form -app.post('/user/consent', oidc.consent()); - -//user creation form -app.get('/user/create', function(req, res, next) { - 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' - } - }; - for(var i in fields) { - inputs += '
    '; - } - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Sign in

    '+inputs+'
    '+error; - res.send(''+head+body+''); -}); - -//process user creation -app.post('/user/create', oidc.use({policies: {loggedIn: false}, models: 'user'}), function(req, res, next) { - 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) { - res.redirect(req.path); - } else { - req.body.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'); - } - }); - } - }); -}); - -app.get('/user', oidc.check(), function(req, res, next){ - res.send('

    User Page

    '); -}); - -//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/bar', oidc.check('bar'), function(req, res, next){ - res.send('

    Page restricted by 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/or', oidc.check(/bar|foo/), function(req, res, next){ - res.send('

    Page restricted by "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) { - inputs += '
    '+fields[i].html+'
    '; - } - var error = req.session.error?'
    '+req.session.error+'
    ':''; - var body = '

    Register Client

    '+inputs+'
    '+error; - res.send(''+head+body+''); - } else if(!err) { - mkId(); - } else { - next(err); - } - }); - }; - mkId(); -}); - -//process client register -app.post('/client/register', oidc.use('client'), function(req, res, next) { - 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){ - if(!err && client) { - res.redirect('/client/'+client.id); - } else { - next(err); - } - }); -}); - -app.get('/client', oidc.use('client'), function(req, res, next){ - var head ='

    Clients Page

    '; - 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

    • Key: '+client.key+'
    • Secret: '+client.secret+'
    • Redirect Uris:
        '; - client.redirect_uris.forEach(function(uri){ - html += '
      • '+uri+'
      • '; - }); - html+='
    '; - res.send(html); - } else { - res.send('

    No Client Fount!

    '); - } - }); -}); - -app.get('/test/clear', function(req, res, next){ - 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 = ''; - 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('')+'
    '); - }); - 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; - 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:
    "; - var 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 '+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+'
    '); - } - }); - }); - - // 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); - } -}); - - - -// development only -if ('development' == app.get('env')) { - app.use(errorHandler()); -} - -function mkFields(params) { - 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; - } - } - } - return fields; -} - - 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')); -}); diff --git a/examples/example-using-sessions.js b/examples/openid-connect-example.js similarity index 99% rename from examples/example-using-sessions.js rename to examples/openid-connect-example.js index 1905a4c..c752b0b 100644 --- a/examples/example-using-sessions.js +++ b/examples/openid-connect-example.js @@ -39,11 +39,7 @@ 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(bodyParser()); app.use(methodOverride()); app.use(cookieParser('Some Secret!!!')); app.use(expressSession({store: new rs({host: '127.0.0.1', port: 6379}), secret: 'Some Secret!!!'})); From 77a9290ac060ea8c417b49a86ecbd67018924e2b Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 16:24:58 -0700 Subject: [PATCH 06/13] Further linting. Also updated displayed text for protected pages. --- .jscsrc | 15 +- examples/README.md | 47 + examples/openid-connect-example.js | 629 ++++----- index.js | 1957 ++++++++++++++-------------- 4 files changed, 1368 insertions(+), 1280 deletions(-) create mode 100644 examples/README.md diff --git a/.jscsrc b/.jscsrc index 6f4c8da..aa10c91 100644 --- a/.jscsrc +++ b/.jscsrc @@ -35,7 +35,6 @@ "=", "==", "===", - "!=", "!==", ">", ">=", @@ -46,38 +45,48 @@ "=", "==", "===", - "!=", "!==", ">", ">=", "<=" ], "disallowSpaceBeforeBinaryOperators": [ + "+", "+", "/", "*", + "!=", + "<", "<" ], "disallowPaddingNewlinesInBlocks": true, - "validateIndentation": 4, + "validateIndentation": 2, "disallowSpaceAfterKeywords": [ + "if", "if", "for", + "for", + "switch", "switch", + "catch", "catch" ], "disallowSpacesInConditionalExpression": { "beforeAlternate": true }, "disallowSpaceAfterBinaryOperators": [ + "+", "+", "/", "*", + "!=", + "<", "<" ], "disallowSpacesInsideObjectBrackets": "all", "disallowSpacesInsideArrayBrackets": "all", "disallowSpaceBeforePostfixUnaryOperators": [ + "++", "++" ] } diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..17143d9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,47 @@ +# 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 at http://localhost:3001/user/create + +http://localhost:3001/client/register + 210 + http://localhost:3001/user + Client Key 0798ec5cb300cd5097d2595319b054fb + Client Secret 805d61ab96708fa4b45fba75bbbfcea6 + +http://localhost:3001/test/clear + scope: foo + Follow prompts + Accept + Next, next, next + See page that is restricted by foo scope: http://localhost:3001/user/foo?access_token=4ace62feb6c768b27f6524b805276858 + +http://localhost:3001/ + +6. Test an auth flow at http://localhost:3001/test + +http://localhost:3001/my/login +http://localhost:3001/logout diff --git a/examples/openid-connect-example.js b/examples/openid-connect-example.js index c752b0b..70700dd 100644 --- a/examples/openid-connect-example.js +++ b/examples/openid-connect-example.js @@ -7,7 +7,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'), @@ -30,12 +30,11 @@ var options = { 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;}}}}, + 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')); @@ -52,6 +51,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 +62,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 +99,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 = '
      '+lis.join('')+'
    '; var error = req.session.error?'
    '+req.session.error+'
    ':''; @@ -112,37 +118,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 +159,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

    '); }); //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 +262,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 +276,259 @@ 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

    '; - 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

    '; + 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

    • Key: '+client.key+'
    • Secret: '+client.secret+'
    • Redirect Uris:
        '; - client.redirect_uris.forEach(function(uri){ - html += '
      • '+uri+'
      • '; - }); - html+='
    '; - res.send(html); - } else { - res.send('

    No Client Fount!

    '); - } +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

    • Key: '+client.key+'
    • Secret: '+client.secret+'
    • Redirect Uris:
        '; + client.redirect_uris.forEach(function(uri) { + html += '
      • '+uri+'
      • '; + }); + html+='
    '; + res.send(html); + } else { + res.send('

    No Client Fount!

    '); + } }); }); -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() { + 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')); + 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 dd2059c..d1356c5 100644 --- a/index.js +++ b/index.js @@ -16,368 +16,368 @@ crypto = require('crypto'), _ = require('lodash'), extend = require('extend'), url = require('url'), -promiseQ = require('q'), +qPromise = require('q'), jwt = require('jwt-simple'), 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'); + } + 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) { - var sha256 = crypto.createHash('sha256'); - if(!values.key) { - sha256.update(values.name); - sha256.update(Math.random()+''); - values.key = sha256.digest('hex'); - } - 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' - }, - 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}, + auth: {model: 'auth', required: true}, + 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); - var 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) { - if(this.settings.policies.hasOwnProperty(i)) { - 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 j in this.settings.alien) { - if(this.settings.alien.hasOwnProperty(j)) { - if(this.settings.models[j]) delete this.settings.models[j]; - } - } + 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 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 - }); + } + + 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) { - if(spec.hasOwnProperty(i)) { - 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 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[i2])) { - error = spec[i2](params); - } - - 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; + } + + 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[i2])) { + error = spec[i2](params); + } + + 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; }; /** @@ -394,35 +394,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); + } + }); + }]; }; /** @@ -436,253 +436,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']), - /*jshint unused:false */ - function(req, res, next) { - promiseQ(req.parsedParams).then(function(params) { - //Step 2: Check if response_type is supported and client_id is valid. - - var deferred = promiseQ.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 = promiseQ.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 = promiseQ.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); - }); + 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); + } + }); - promiseQ.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.'); } - ]; + } + } + ]; }; /** @@ -1102,26 +1107,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'), - - /*jshint unused:false */ - 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}); + } + }); + } + ]; }; /** @@ -1135,66 +1140,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; }; From c367a6adc1abbca5b04710e504206d9095626c78 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 16:32:36 -0700 Subject: [PATCH 07/13] Clean up. --- examples/README.md | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/examples/README.md b/examples/README.md index 17143d9..880de3e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,24 +24,14 @@ npm install node openid-connect-example.js ``` -5. Create user at http://localhost:3001/user/create - -http://localhost:3001/client/register - 210 - http://localhost:3001/user - Client Key 0798ec5cb300cd5097d2595319b054fb - Client Secret 805d61ab96708fa4b45fba75bbbfcea6 - -http://localhost:3001/test/clear - scope: foo - Follow prompts - Accept - Next, next, next - See page that is restricted by foo scope: http://localhost:3001/user/foo?access_token=4ace62feb6c768b27f6524b805276858 - -http://localhost:3001/ - -6. Test an auth flow at http://localhost:3001/test - -http://localhost:3001/my/login -http://localhost:3001/logout +5. Create user: http://localhost:3001/user/create +6. Register a client: http://localhost:3001/client/register +7. Navigate to http://localhost:3001/test/clear + * Select scope: foo + * Follow prompts + - Accept + - Next, next, next + - See page that is restricted by foo scope +8. Test an auth flow at http://localhost:3001/test +9. Logout: http://localhost:3001/logout +10. Login again: http://localhost:3001/my/login From 4da25e59b6f6394fd47bc5ee2be59f168e740dd1 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 16:36:53 -0700 Subject: [PATCH 08/13] Fix list numbering --- examples/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/README.md b/examples/README.md index 880de3e..57398a7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,24 +5,24 @@ You must have [Node.js](http://nodejs.org/) and [Redis](http://redis.io/download 1. Fork this repository 2. Clone it, using your username -``` -git clone git@github.com:/OpenIDConnect.git -cd OpenIDConnect -``` + ``` + git clone git@github.com:/OpenIDConnect.git + cd OpenIDConnect + ``` 3. Install Node.js dependencies -``` -npm install -cd examples -npm install -``` + ``` + npm install + cd examples + npm install + ``` 4. Start server -``` -node openid-connect-example.js -``` + ``` + node openid-connect-example.js + ``` 5. Create user: http://localhost:3001/user/create 6. Register a client: http://localhost:3001/client/register From 1c6b91ebc7e97ec94bf8aa38723ecc7a0dd303dd Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 20:50:25 -0700 Subject: [PATCH 09/13] Debug waterline validation error. --- examples/README.md | 17 +++++---- examples/openid-connect-example.js | 20 ++++++++--- index.js | 58 +++++++++++++++--------------- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/examples/README.md b/examples/README.md index 880de3e..12a53ae 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,12 +26,15 @@ node openid-connect-example.js 5. Create user: http://localhost:3001/user/create 6. Register a client: http://localhost:3001/client/register -7. Navigate to http://localhost:3001/test/clear - * Select scope: foo + * 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 - - Next, next, next - - See page that is restricted by foo scope -8. Test an auth flow at http://localhost:3001/test -9. Logout: http://localhost:3001/logout -10. Login again: http://localhost:3001/my/login + - Get Token + - Get Resource + - You should see page that is restricted by ```foo``` scope +8. Logout: http://localhost:3001/logout?access_token= +12. Logout; then navigate to http://localhost:3001/user/foo?access_token= diff --git a/examples/openid-connect-example.js b/examples/openid-connect-example.js index 70700dd..6d9838b 100644 --- a/examples/openid-connect-example.js +++ b/examples/openid-connect-example.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -29,7 +28,7 @@ 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. + //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 }; @@ -38,10 +37,20 @@ 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 @@ -420,12 +429,14 @@ app.get('/test', oidc.use({policies: {loggedIn: false}, models: 'client'}), func 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'), @@ -448,7 +459,6 @@ app.get('/test', oidc.use({policies: {loggedIn: false}, models: 'client'}), func console.log('Response: '+chunk); }); pres.on('end', function() { - console.log(data); try { data = JSON.parse(data); html += 'Got:
    '+JSON.stringify(data)+'
    '; diff --git a/index.js b/index.js index d1356c5..879b5b8 100644 --- a/index.js +++ b/index.js @@ -118,8 +118,7 @@ var defaults = { sha256.update(values.name); sha256.update(Math.random()+''); values.key = sha256.digest('hex'); - } - if(!values.secret) { + } else if(!values.secret) { sha256.update(values.key); sha256.update(values.name); sha256.update(Math.random()+''); @@ -181,7 +180,11 @@ var defaults = { attributes: { token: {type: 'string', required: true}, scope: {type: 'array', required: true}, - auth: {model: 'auth', 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} } } @@ -791,35 +794,34 @@ OpenIDConnect.prototype.token = function() { case 'authorization_code': //Step 3: check if code is valid and not used previously req.model.auth.findOne({code: params.code}) - .populate('accessTokens') - .populate('refreshTokens') - .populate('client') - .exec(function(err, auth) { - if(!err && auth) { - if(auth.status!='created') { - auth.refresh.forEach(function(refresh) { - refresh.destroy(); - }); - auth.access.forEach(function(access) { - access.destroy(); - }); - auth.destroy(); - deferred.reject({type: 'error', error: 'invalid_grant', msg: 'Authorization code already used.'}); - } else { - //obj.auth = a; - deferred.resolve({auth: auth, scope: auth.scope, client: client, user: auth.user, sub: auth.sub}); - } - } else { - deferred.reject({type: 'error', error: 'invalid_grant', msg: 'Authorization code is invalid.'}); - } - }); + .populate('accessTokens') + .populate('refreshTokens') + .populate('client') + .exec(function(err, auth) { + if(!err && auth) { + if(auth.status!='created') { + auth.refresh.forEach(function(refresh) { + refresh.destroy(); + }); + auth.access.forEach(function(access) { + access.destroy(); + }); + auth.destroy(); + deferred.reject({type: 'error', error: 'invalid_grant', msg: 'Authorization code already used.'}); + } else { + //obj.auth = a; + deferred.resolve({auth: auth, scope: auth.scope, client: client, user: auth.user, sub: auth.sub}); + } + } else { + deferred.reject({type: 'error', error: 'invalid_grant', msg: 'Authorization code is invalid.'}); + } + }); - // linter complains about unreachable break after return without this if + // linter complains about unreachable break after return without this if if(true) { - //Extra checks, required if grant_type is 'authorization_code' + //Extra checks, required if grant_type is 'authorization_code' return deferred.promise.then(function(obj) { //Step 4: check if grant_type is valid - if(obj.auth.responseType!='code') { throw {type: 'error', error: 'unauthorized_client', msg: 'Client cannot use this grant type.'}; } From afcf04fc4b90e01b90b21032acbb1da5001edbd1 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 21:04:10 -0700 Subject: [PATCH 10/13] Update README.md --- examples/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index fc305d7..e922086 100644 --- a/examples/README.md +++ b/examples/README.md @@ -36,5 +36,5 @@ You must have [Node.js](http://nodejs.org/) and [Redis](http://redis.io/download - Get Token - Get Resource - You should see page that is restricted by ```foo``` scope -8. Logout: http://localhost:3001/logout?access_token= -12. Logout; then navigate to http://localhost:3001/user/foo?access_token= +8. Logout: http://localhost:3001/logout?access_token=YOUR_ACCESS_TOKEN +12. Logout; then navigate to http://localhost:3001/user/foo?access_token=YOUR_ACCESS_TOKEN From 1a1cf711d2cdb1c3e6176038463c20efeb9c13ab Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 21:08:26 -0700 Subject: [PATCH 11/13] Update README.md --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index e922086..b085a47 100644 --- a/examples/README.md +++ b/examples/README.md @@ -37,4 +37,4 @@ You must have [Node.js](http://nodejs.org/) and [Redis](http://redis.io/download - Get Resource - You should see page that is restricted by ```foo``` scope 8. Logout: http://localhost:3001/logout?access_token=YOUR_ACCESS_TOKEN -12. Logout; then navigate to http://localhost:3001/user/foo?access_token=YOUR_ACCESS_TOKEN +9. Navigate to http://localhost:3001/user/foo?access_token=YOUR_ACCESS_TOKEN From 4d4a6bedc266b9419e9a7f258695b15f706e2eb3 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 21:11:34 -0700 Subject: [PATCH 12/13] Minor clean up. --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 879b5b8..c1332f0 100644 --- a/index.js +++ b/index.js @@ -142,7 +142,7 @@ var defaults = { connection: 'def', policies: 'loggedIn', attributes: { - client: {model: 'client', required: true}, + client: {model: 'client', required: true}, scope: {type: 'array', required: true}, user: {model: 'user', required: true}, sub: {type: 'string', required: true}, @@ -183,8 +183,8 @@ var defaults = { // 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'}, + auth: {model: 'auth', required: true}, + //auth: {model: 'auth'}, status: {type: 'string', required: true} } } From a61967713506269df66c4658749f859b5eb7b819 Mon Sep 17 00:00:00 2001 From: Anders Riutta Date: Sun, 22 Mar 2015 21:16:06 -0700 Subject: [PATCH 13/13] Re-enable tmp fix for waterline validation error --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index c1332f0..3b9808b 100644 --- a/index.js +++ b/index.js @@ -183,8 +183,8 @@ var defaults = { // 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'}, + //auth: {model: 'auth', required: true}, + auth: {model: 'auth'}, status: {type: 'string', required: true} } }