├── test ├── app.js ├── promise.test.js └── password.test.js ├── lib ├── views │ └── auth-fail.jade ├── restler.js ├── routeTriggeredSequence.js ├── modules │ ├── dropbox.js │ ├── vimeo.js │ ├── justintv.js │ ├── ldap.js │ ├── github.js │ ├── readability.js │ ├── tumblr.js │ ├── gowalla.js │ ├── foursquare.js │ ├── yahoo.js │ ├── instagram.js │ ├── twitter.js │ ├── facebook.js │ ├── google.js │ ├── linkedin.js │ ├── googlehybrid.js │ ├── openid.js │ ├── box.js │ ├── oauth2.js │ ├── oauth.js │ ├── everymodule.js │ └── password.js ├── utils.js ├── expressHelper.js ├── promise.js ├── step.js └── stepSequence.js ├── .gitignore ├── Makefile ├── example ├── views │ ├── layout.jade │ ├── login.jade │ ├── register.jade │ └── home.jade ├── conf.js └── server.js ├── package.json ├── index.js └── README.md /test/app.js: -------------------------------------------------------------------------------- 1 | ../example/server.js -------------------------------------------------------------------------------- /lib/views/auth-fail.jade: -------------------------------------------------------------------------------- 1 | p Your authentication failed because: #{errorDescription} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.swp 2 | node_modules 3 | test/creds.js 4 | issues 5 | IRC.md 6 | playground.js 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = $(shell find test/ -name '*.test.js') 2 | 3 | test: 4 | node $(TESTS) 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /example/views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | title= typeof(title) !== 'undefined' ? title : "everyauth example" 5 | script(src='http://static.ak.fbcdn.net/connect/en_US/core.js') 6 | body 7 | h1 everyauth Example 8 | #main 9 | != body 10 | -------------------------------------------------------------------------------- /test/promise.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | , Promise = require('../lib/promise'); 3 | 4 | module.exports = { 5 | 'fulfill called >1 times should only have an effect once': function (done) { 6 | var p = new Promise() 7 | , test = null; 8 | p.callback( function (val) { 9 | test = val; 10 | }); 11 | p.fulfill(1); 12 | p.fulfill(2); 13 | setTimeout( function () { 14 | test.should.equal(1); 15 | done(); 16 | }, 1000); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /example/views/login.jade: -------------------------------------------------------------------------------- 1 | - if ('undefined' !== typeof errors && errors.length) 2 | ul#errors 3 | - each error in errors 4 | li.error= error 5 | form(action='/login', method='post') 6 | #login 7 | label(for=everyauth.password.loginFormFieldName) Login 8 | input(type='text', name=everyauth.password.loginFormFieldName, value=email) 9 | #password 10 | label(for=everyauth.password.passwordFormFieldName) Password 11 | input(type='password', name=everyauth.password.passwordFormFieldName) 12 | #submit 13 | input(type='submit') Login 14 | -------------------------------------------------------------------------------- /lib/restler.js: -------------------------------------------------------------------------------- 1 | var rest = module.exports = require('restler'); 2 | 3 | // https 'end' event patch for restler -- see https://github.com/bnoguchi/everyauth/issues/16 4 | var Request = rest.Request; 5 | 6 | if (!Request.__responseHandler__) { 7 | // alias method chain this only once 8 | 9 | var proto = Request.prototype; 10 | 11 | proto.__responseHandler__ = proto._responseHandler; 12 | 13 | proto._responseHandler = function (response) { 14 | response.on('close', function () { 15 | response.emit('end'); 16 | }); 17 | this.__responseHandler__(response); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /example/views/register.jade: -------------------------------------------------------------------------------- 1 | h2 Register 2 | - if ('undefined' !== typeof errors && errors.length) 3 | ul#errors 4 | - each error in errors 5 | li.error= error 6 | form(action='/register', method='post') 7 | #login 8 | label(for=everyauth.password.loginFormFieldName) Login 9 | input(type='text', name=everyauth.password.loginFormFieldName, value=userParams[everyauth.password.loginFormFieldName]) 10 | #password 11 | label(for=everyauth.password.passwordFormFieldName) Password 12 | input(type='password', name=everyauth.password.passwordFormFieldName) 13 | #submit 14 | input(type='submit') Register 15 | -------------------------------------------------------------------------------- /lib/routeTriggeredSequence.js: -------------------------------------------------------------------------------- 1 | var StepSequence = require('./stepSequence'); 2 | 3 | var RouteTriggeredSequence = module.exports = function RouteTriggeredSequence (name, _module) { 4 | StepSequence.call(this, name, _module); 5 | } 6 | 7 | RouteTriggeredSequence.prototype.__proto__ = StepSequence.prototype; 8 | 9 | RouteTriggeredSequence.prototype.routeHandler = function () { 10 | // Create a shallow clone, so that 11 | // seq.values are different per 12 | // HTTP request 13 | var seq = this.materialize(); 14 | // Kicks off a sequence of steps based on 15 | // a route. 16 | seq.start.apply(seq, arguments); // BOOM! 17 | }; 18 | -------------------------------------------------------------------------------- /lib/modules/dropbox.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth'); 2 | 3 | var dropbox = module.exports = 4 | oauthModule.submodule('dropbox') 5 | .apiHost('https://api.dropbox.com/0') 6 | .oauthHost('https://www.dropbox.com/0') 7 | .entryPath('/auth/dropbox') 8 | .callbackPath('/auth/dropbox/callback') 9 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 10 | var p = this.Promise(); 11 | this.oauth.get(this.apiHost() + '/account/info', accessToken, accessTokenSecret, function (err, data) { 12 | if (err) return p.fail(err); 13 | var oauthUser = JSON.parse(data); 14 | oauthUser.id = oauthUser.uid; 15 | p.fulfill(oauthUser); 16 | }); 17 | return p; 18 | }); 19 | -------------------------------------------------------------------------------- /lib/modules/vimeo.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth'); 2 | 3 | var vimeo = module.exports = 4 | oauthModule.submodule('vimeo') 5 | 6 | .apiHost('http://vimeo.com/api/rest/v2') 7 | .oauthHost('http://vimeo.com') 8 | 9 | .entryPath('/auth/vimeo') 10 | .callbackPath('/auth/vimeo/callback') 11 | 12 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 13 | var promise = this.Promise(); 14 | this.oauth.get(this.apiHost() + '?format=json&method=vimeo.people.getInfo&user_id=' + accessTokenSecret, accessToken, accessTokenSecret, function (err, data) { 15 | if (err) return promise.fail(err); 16 | var oauthUser = JSON.parse(data); 17 | return promise.fulfill(oauthUser.person); 18 | }); 19 | return promise; 20 | }); 21 | -------------------------------------------------------------------------------- /lib/modules/justintv.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth'); 2 | 3 | module.exports = 4 | oauthModule.submodule('justintv') 5 | 6 | .apiHost('http://api.justin.tv') 7 | .oauthHost('http://api.justin.tv') 8 | 9 | .entryPath('/auth/justintv') 10 | .callbackPath('/auth/justintv/callback') 11 | 12 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 13 | var promise = this.Promise(); 14 | this.oauth.get(this.apiHost() + '/api/account/whoami.json', accessToken, accessTokenSecret, function (err, data) { 15 | if (err) return promise.fail(err); 16 | var oauthUser = JSON.parse(data); 17 | return promise.fulfill(oauthUser); 18 | }); 19 | return promise; 20 | }) 21 | 22 | .convertErr( function (data) { 23 | return new Error(data.data); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/modules/ldap.js: -------------------------------------------------------------------------------- 1 | var passwordModule = require('./password'); 2 | 3 | var ldap = module.exports = 4 | passwordModule.submodule('ldap') 5 | .configurable({ 6 | host: 'the ldap host' 7 | , port: 'the ldap port' 8 | }) 9 | .authenticate( function (login, password, req, res) { 10 | var promise = this.Promise(); 11 | ldapauth.authenticate(this.host(), this.port(), login, password, function (err, result) { 12 | var user, errors; 13 | if (err) { 14 | return promise.fail(err); 15 | } 16 | if (result === false) { 17 | errors = ['Login failed.']; 18 | return promise.fulfill(errors); 19 | } else if (result === true) { 20 | user = {}; 21 | user[this.loginKey()] = login; 22 | return promise.fulfill(user); 23 | } else { 24 | throw new Error('ldapauth returned a result that was neither `true` nor `false`'); 25 | } 26 | }); 27 | return promise; 28 | }); 29 | -------------------------------------------------------------------------------- /lib/modules/github.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth2'); 2 | 3 | var github = module.exports = 4 | oauthModule.submodule('github') 5 | .configurable({ 6 | scope: 'specify types of access: (no scope), user, public_repo, repo, gist' 7 | }) 8 | 9 | .oauthHost('https://github.com') 10 | .apiHost('https://github.com/api/v2/json') 11 | 12 | .authPath('/login/oauth/authorize') 13 | .accessTokenPath('/login/oauth/access_token') 14 | 15 | .entryPath('/auth/github') 16 | .callbackPath('/auth/github/callback') 17 | 18 | .authQueryParam('scope', function () { 19 | return this._scope && this.scope(); 20 | }) 21 | 22 | .fetchOAuthUser( function (accessToken) { 23 | var p = this.Promise(); 24 | this.oauth.get(this.apiHost() + '/user/show', accessToken, function (err, data) { 25 | if (err) return p.fail(err); 26 | var oauthUser = JSON.parse(data).user; 27 | p.fulfill(oauthUser); 28 | }) 29 | return p; 30 | }); 31 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | tls = require('tls'); 2 | 3 | var clone = exports.clone = function clone (obj) { 4 | if (obj === undefined || obj === null) 5 | return obj 6 | if (Array.isArray(obj)) 7 | return cloneArray(obj); 8 | if (obj.constructor == Object) 9 | return cloneObject(obj); 10 | return obj; 11 | }; 12 | 13 | function cloneObject (obj, shouldMinimizeData) { 14 | var ret = {}; 15 | for (var k in obj) 16 | ret[k] = clone(obj[k]); 17 | return ret; 18 | }; 19 | 20 | function cloneArray (arr) { 21 | var ret = []; 22 | for (var i = 0, l = arr.length; i < l; i++) 23 | ret.push(clone(arr[i])); 24 | return ret; 25 | }; 26 | 27 | exports.extractHostname = function (req) { 28 | var headers = req.headers 29 | , protocol = (req.connection.server instanceof tls.Server || req.headers['x-forwarded-proto'] == 'https') 30 | ? 'https://' 31 | : 'http://' 32 | , host = headers.host; 33 | return protocol + host; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/modules/readability.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth'); 2 | 3 | var readability = module.exports = 4 | oauthModule.submodule('readability') 5 | 6 | .apiHost('https://www.readability.com/api/rest/v1') 7 | .oauthHost('https://www.readability.com/api/rest/v1/oauth') 8 | 9 | .requestTokenPath('/request_token') 10 | .authorizePath('/authorize') 11 | .accessTokenPath('/access_token') 12 | 13 | .entryPath('/auth/readability') 14 | .callbackPath('/auth/readability/callback') 15 | 16 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 17 | var p = this.Promise(); 18 | this.oauth.get(this.apiHost() + '/users/_current', accessToken, accessTokenSecret, function (err, data) { 19 | if (err) return p.fail(err.error_message); 20 | var oauthUser = JSON.parse(data); 21 | p.fulfill(oauthUser); 22 | }) 23 | return p; 24 | }) 25 | .convertErr( function (data) { 26 | return new Error(data.error_message); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /lib/modules/tumblr.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth') 2 | , Parser = require('xml2js').Parser; 3 | 4 | var twitter = module.exports = 5 | oauthModule.submodule('tumblr') 6 | .apiHost('http://www.tumblr.com/api') 7 | .oauthHost('http://www.tumblr.com') 8 | .entryPath('/auth/tumblr') 9 | .callbackPath('/auth/tumblr/callback') 10 | .sendCallbackWithAuthorize(false) 11 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 12 | var promise = this.Promise(); 13 | this.oauth.get(this.apiHost() + '/authenticate', accessToken, accessTokenSecret, function (err, data) { 14 | if (err) return promise.fail(err); 15 | var parser = new Parser(); 16 | parser.on('end', function (result) { 17 | var oauthUser = result.tumblelog['@']; 18 | promise.fulfill(oauthUser); 19 | }); 20 | parser.parseString(data); 21 | }); 22 | return promise; 23 | }) 24 | .convertErr( function (data) { 25 | return data.data; 26 | }); 27 | -------------------------------------------------------------------------------- /lib/expressHelper.js: -------------------------------------------------------------------------------- 1 | module.exports = function (app) { 2 | var everyauth = this; 3 | app.dynamicHelpers({ 4 | everyauth: function (req, res) { 5 | var ea = {} 6 | , sess = req.session; 7 | ea.loggedIn = sess.auth && !!sess.auth.loggedIn; 8 | 9 | // Copy the session.auth properties over 10 | var auth = sess.auth; 11 | for (var k in auth) { 12 | ea[k] = auth[k]; 13 | } 14 | 15 | // Add in access to loginFormFieldName() and passwordFormFieldName() 16 | // TODO Don't compute this if we 17 | // aren't using password module 18 | ea.password || (ea.password = {}); 19 | ea.password.loginFormFieldName = everyauth.password.loginFormFieldName(); 20 | ea.password.passwordFormFieldName = everyauth.password.passwordFormFieldName(); 21 | 22 | ea.user = req.user; 23 | 24 | return ea; 25 | } 26 | , user: function (req, res) { 27 | return req.user; 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "everyauth", 3 | "description": "Auth solution (password, facebook, & more) for your node.js Connect & Express apps", 4 | "version": "0.2.23", 5 | "homepage": "https://github.com/bnoguchi/everyauth/", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/bnoguchi/everyauth.git" 9 | }, 10 | "author": "Brian Noguchi (https://github.com/bnoguchi/)", 11 | "keywords": [ 12 | "auth", 13 | "oauth", 14 | "password", 15 | "facebook", 16 | "openid", 17 | "twitter", 18 | "authorization", 19 | "authentication", 20 | "connect", 21 | "express" 22 | ], 23 | "main": "./index.js", 24 | "directories": { 25 | "lib": "lib" 26 | }, 27 | "dependencies": { 28 | "oauth": ">=0.9.0", 29 | "restler": ">=0.2.1", 30 | "connect": ">=1 <2", 31 | "openid": "=0.1.8", 32 | "xml2js": ">=0.1.7" 33 | }, 34 | "devDependencies": { 35 | "express": ">=2.3.10", 36 | "jade": ">=0.12.1", 37 | "tobi": ">=0.2.2", 38 | "expresso": ">=0.8.1", 39 | "should": ">=0.2.1" 40 | }, 41 | "engines": { 42 | "node": ">=0.4.0" 43 | } 44 | } -------------------------------------------------------------------------------- /lib/modules/gowalla.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth2') 2 | , rest = require('restler'); 3 | 4 | var gowalla = module.exports = 5 | oauthModule.submodule('gowalla') 6 | .apiHost('https://api.gowalla.com') 7 | .oauthHost('https://gowalla.com') 8 | 9 | .authPath('/api/oauth/new') 10 | .accessTokenPath('https://api.gowalla.com/api/oauth/token') 11 | 12 | .entryPath('/auth/gowalla') 13 | .callbackPath('/auth/gowalla/callback') 14 | 15 | .accessTokenHttpMethod('post') 16 | .postAccessTokenParamsVia('data') 17 | .accessTokenParam('grant_type', 'authorization_code') 18 | 19 | .fetchOAuthUser( function (accessToken) { 20 | var promise = this.Promise(); 21 | rest.get(this._apiHost + '/users/me', { 22 | query: { oauth_token: accessToken }, 23 | headers: { 24 | "X-Gowalla-API-Key": this.appId(), 25 | "Accept": "application/json" 26 | } 27 | }).on('success', function (data, res) { 28 | promise.fulfill(data); 29 | }).on('error', function (data, res) { 30 | promise.fail(data); 31 | }); 32 | 33 | return promise; 34 | }) 35 | 36 | .convertErr( function (data) { 37 | return new Error(data); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/modules/foursquare.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth2') 2 | , rest = require('../restler'); 3 | 4 | var foursquare = module.exports = 5 | oauthModule.submodule('foursquare') 6 | .apiHost('https://api.foursquare.com/v2') 7 | .oauthHost('https://foursquare.com') 8 | 9 | .authPath('/oauth2/authenticate') 10 | .accessTokenPath('/oauth2/access_token') 11 | 12 | .entryPath('/auth/foursquare') 13 | .callbackPath('/auth/foursquare/callback') 14 | 15 | .authQueryParam('response_type', 'code') 16 | 17 | .accessTokenHttpMethod('get') 18 | .accessTokenParam('grant_type', 'authorization_code') 19 | 20 | .fetchOAuthUser( function (accessToken) { 21 | var promise = this.Promise(); 22 | rest.get(this.apiHost() + '/users/self', { 23 | query: { oauth_token: accessToken } 24 | }).on('success', function (data, res) { 25 | var oauthUser = data.response.user; 26 | promise.fulfill(oauthUser); 27 | }).on('error', function (data, res) { 28 | promise.fail(data); 29 | }); 30 | return promise; 31 | }) 32 | 33 | .convertErr( function (data) { 34 | var errMsg = JSON.parse(data.data).meta.errorDetail; 35 | return new Error(errMsg); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/modules/yahoo.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth') 2 | , OAuth = require('oauth').OAuth; 3 | 4 | var yahoo = module.exports = 5 | oauthModule.submodule('yahoo') 6 | .definit( function () { 7 | var oauth = this.oauth = new OAuth( 8 | this.oauthHost() + this.requestTokenPath() 9 | , this.oauthHost() + this.accessTokenPath() 10 | , this.consumerKey() 11 | , this.consumerSecret() 12 | , '1.0', null, 'HMAC-SHA1'); 13 | }) 14 | .apiHost('http://social.yahooapis.com/v1') 15 | .oauthHost('https://api.login.yahoo.com/oauth/v2') 16 | 17 | .requestTokenPath('/get_request_token') 18 | .accessTokenPath('/get_token') 19 | .authorizePath('/request_auth') 20 | 21 | .entryPath('/auth/yahoo') 22 | .callbackPath('/auth/yahoo/callback') 23 | 24 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 25 | var promise = this.Promise(); 26 | this.oauth.get(this.apiHost() + '/user/' + params.xoauth_yahoo_guid + '/profile?format=json', accessToken, accessTokenSecret, function (err, data) { 27 | if (err) return promise.fail(err); 28 | var oauthUser = JSON.parse(data).profile; 29 | promise.fulfill(oauthUser); 30 | }); 31 | return promise; 32 | }); 33 | -------------------------------------------------------------------------------- /lib/modules/instagram.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth2') 2 | , querystring= require('querystring'); 3 | 4 | var instagram = module.exports = 5 | oauthModule.submodule('instagram') 6 | .configurable({ 7 | display: 'set to "touch" if you want users to see a mobile optimized version of the auth page' 8 | , scope: 'specify types of access (space separated if > 1): basic (default), comments, relationships, likes' 9 | }) 10 | 11 | .oauthHost('https://api.instagram.com') 12 | .apiHost('https://api.instagram.com/v1') 13 | 14 | .entryPath('/auth/instagram') 15 | .callbackPath('/auth/instagram/callback') 16 | 17 | .authQueryParam('response_type', 'code') 18 | .authQueryParam('display', function () { 19 | return this._display && this.display(); 20 | }) 21 | .authQueryParam('scope', function () { 22 | return this._scope && this.scope(); 23 | }) 24 | 25 | .accessTokenParam('grant_type', 'authorization_code') 26 | .postAccessTokenParamsVia('data') 27 | 28 | .fetchOAuthUser( function (accessToken) { 29 | var p = this.Promise(); 30 | this.oauth.get(this.apiHost() + '/users/self', accessToken, function (err, data) { 31 | if (err) return p.fail(err.error_message); 32 | var oauthUser = JSON.parse(data).data; 33 | p.fulfill(oauthUser); 34 | }) 35 | return p; 36 | }) 37 | .convertErr( function (data) { 38 | return new Error(data.error_message); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/modules/twitter.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth') 2 | , url = require('url'); 3 | 4 | var twitter = module.exports = 5 | oauthModule.submodule('twitter') 6 | .apiHost('https://api.twitter.com') 7 | .oauthHost('https://api.twitter.com') 8 | .entryPath('/auth/twitter') 9 | .callbackPath('/auth/twitter/callback') 10 | .authorizePath('/oauth/authenticate') 11 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 12 | var promise = this.Promise(); 13 | this.oauth.get(this.apiHost() + '/users/show.json?user_id=' + params.user_id, accessToken, accessTokenSecret, function (err, data) { 14 | if (err) return promise.fail(err); 15 | var oauthUser = JSON.parse(data); 16 | promise.fulfill(oauthUser); 17 | }); 18 | return promise; 19 | }) 20 | .authCallbackDidErr( function (req) { 21 | var parsedUrl = url.parse(req.url, true); 22 | return parsedUrl.query && !!parsedUrl.query.denied; 23 | }) 24 | .handleAuthCallbackError( function (req, res) { 25 | if (res.render) { 26 | res.render(__dirname + '/../views/auth-fail.jade', { 27 | errorDescription: 'The user denied your request' 28 | }); 29 | } else { 30 | // TODO Replace this with a nice fallback 31 | throw new Error("You must configure handleAuthCallbackError if you are not using express"); 32 | } 33 | }) 34 | .convertErr( function (data) { 35 | return new Error(data.data.match(/(.+)<\/error>/)[1]); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/modules/facebook.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth2') 2 | , url = require('url'); 3 | 4 | var fb = module.exports = 5 | oauthModule.submodule('facebook') 6 | .configurable({ 7 | scope: 'specify types of access: See http://developers.facebook.com/docs/authentication/permissions/' 8 | }) 9 | 10 | .apiHost('https://graph.facebook.com') 11 | .oauthHost('https://graph.facebook.com') 12 | 13 | .authPath('https://www.facebook.com/dialog/oauth') 14 | 15 | .entryPath('/auth/facebook') 16 | .callbackPath('/auth/facebook/callback') 17 | 18 | .authQueryParam('scope', function () { 19 | return this._scope && this.scope(); 20 | }) 21 | 22 | .authCallbackDidErr( function (req) { 23 | var parsedUrl = url.parse(req.url, true); 24 | return parsedUrl.query && !!parsedUrl.query.error; 25 | }) 26 | .handleAuthCallbackError( function (req, res) { 27 | var parsedUrl = url.parse(req.url, true) 28 | , errorDesc = parsedUrl.query.error_description; 29 | if (res.render) { 30 | res.render(__dirname + '/../views/auth-fail.jade', { 31 | errorDescription: errorDesc 32 | }); 33 | } else { 34 | // TODO Replace this with a nice fallback 35 | throw new Error("You must configure handleAuthCallbackError if you are not using express"); 36 | } 37 | }) 38 | 39 | .fetchOAuthUser( function (accessToken) { 40 | var p = this.Promise(); 41 | this.oauth.get(this.apiHost() + '/me', accessToken, function (err, data) { 42 | if (err) 43 | return p.fail(err); 44 | var oauthUser = JSON.parse(data); 45 | p.fulfill(oauthUser); 46 | }) 47 | return p; 48 | }) 49 | .convertErr( function (data) { 50 | return new Error(JSON.parse(data.data).error.message); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/modules/google.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth2') 2 | , url = require('url') 3 | , rest = require('../restler'); 4 | 5 | var google = module.exports = 6 | oauthModule.submodule('google') 7 | .configurable({ 8 | scope: "URL identifying the Google service to be accessed. See the documentation for the API you'd like to use for what scope to specify. To specify more than one scope, list each one separated with a space." 9 | }) 10 | 11 | .oauthHost('https://accounts.google.com') 12 | .apiHost('https://www.google.com/m8/feeds') 13 | 14 | .authPath('/o/oauth2/auth') 15 | .authQueryParam('response_type', 'code') 16 | 17 | .accessTokenPath('/o/oauth2/token') 18 | .accessTokenParam('grant_type', 'authorization_code') 19 | .accessTokenHttpMethod('post') 20 | .postAccessTokenParamsVia('data') 21 | 22 | .entryPath('/auth/google') 23 | .callbackPath('/auth/google/callback') 24 | 25 | .authQueryParam('scope', function () { 26 | return this._scope && this.scope(); 27 | }) 28 | 29 | .authCallbackDidErr( function (req) { 30 | var parsedUrl = url.parse(req.url, true); 31 | return parsedUrl.query && !!parsedUrl.query.error; 32 | }) 33 | 34 | .handleAuthCallbackError( function (req, res) { 35 | var parsedUrl = url.parse(req.url, true) 36 | , errorDesc = parsedUrl.query.error + "; " + parsedUrl.query.error_description; 37 | if (res.render) { 38 | res.render(__dirname + '/../views/auth-fail.jade', { 39 | errorDescription: errorDesc 40 | }); 41 | } else { 42 | // TODO Replace this with a nice fallback 43 | throw new Error("You must configure handleAuthCallbackError if you are not using express"); 44 | } 45 | }) 46 | .convertErr( function (data) { 47 | return new Error(data.data.match(/H1>(.+)<\/H1/)[1]); 48 | }) 49 | 50 | .fetchOAuthUser( function (accessToken) { 51 | var promise = this.Promise(); 52 | rest.get(this.apiHost() + '/contacts/default/full', { 53 | query: { oauth_token: accessToken, alt: 'json' } 54 | }).on('success', function (data, res) { 55 | var oauthUser = { id: data.feed.id.$t }; 56 | promise.fulfill(oauthUser); 57 | }).on('error', function (data, res) { 58 | promise.fail(data); 59 | }); 60 | return promise; 61 | }); 62 | -------------------------------------------------------------------------------- /lib/modules/linkedin.js: -------------------------------------------------------------------------------- 1 | var oauthModule = require('./oauth') 2 | , OAuth = require('oauth').OAuth; 3 | 4 | var linkedin = module.exports = 5 | oauthModule.submodule('linkedin') 6 | .definit( function () { 7 | this.oauth = new OAuth( 8 | this.oauthHost() + this.requestTokenPath() 9 | , this.oauthHost() + this.accessTokenPath() 10 | , this.consumerKey() 11 | , this.consumerSecret() 12 | , '1.0', null, 'HMAC-SHA1', null 13 | , { 14 | Accept: '/' 15 | , Connection: 'close' 16 | , 'User-Agent': 'Node authentication' 17 | , 'x-li-format': 'json' // So we get JSON responses 18 | }); 19 | }) 20 | 21 | .apiHost('https://api.linkedin.com/v1') 22 | .oauthHost('https://api.linkedin.com') 23 | 24 | .requestTokenPath('/uas/oauth/requestToken') 25 | .authorizePath('/uas/oauth/authorize') 26 | .accessTokenPath('/uas/oauth/accessToken') 27 | 28 | .entryPath('/auth/linkedin') 29 | .callbackPath('/auth/linkedin/callback') 30 | 31 | .redirectToProviderAuth( function (res, token) { 32 | res.writeHead(303, { 'Location': 'https://www.linkedin.com' + this.authorizePath() + '?oauth_token=' + token }); 33 | res.end(); 34 | }) 35 | 36 | .fetchOAuthUser( function (accessToken, accessTokenSecret, params) { 37 | var promise = this.Promise(); 38 | this.oauth.get(this.apiHost() + '/people/~:(id,first-name,last-name,headline,location:(name,country:(code)),industry,num-connections,num-connections-capped,summary,specialties,proposal-comments,associations,honors,interests,positions,publications,patents,languages,skills,certifications,educations,three-current-positions,three-past-positions,num-recommenders,recommendations-received,phone-numbers,im-accounts,twitter-accounts,date-of-birth,main-address,member-url-resources,picture-url,site-standard-profile-request:(url),api-standard-profile-request:(url,headers),public-profile-url)', accessToken, accessTokenSecret, function (err, data) { 39 | if (err) return promise.fail(err); 40 | var oauthUser = JSON.parse(data); 41 | promise.fulfill(oauthUser); 42 | }); 43 | return promise; 44 | }) 45 | .convertErr( function (data) { 46 | // var errJson = JSON.parse(data.data) 47 | // , errMsg = errJson.message; 48 | var errMsg = data.data; 49 | return new Error("LinkedIn sent back a " + data.statusCode + " response with data = " + errMsg); 50 | }); 51 | -------------------------------------------------------------------------------- /example/conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fb: { 3 | appId: '111565172259433' 4 | , appSecret: '85f7e0a0cc804886180b887c1f04a3c1' 5 | } 6 | , twit: { 7 | consumerKey: 'JLCGyLzuOK1BjnKPKGyQ' 8 | , consumerSecret: 'GNqKfPqtzOcsCtFbGTMqinoATHvBcy1nzCTimeA9M0' 9 | } 10 | , github: { 11 | appId: '11932f2b6d05d2a5fa18' 12 | , appSecret: '2603d1bc663b74d6732500c1e9ad05b0f4013593' 13 | } 14 | , instagram: { 15 | clientId: 'be147b077ddf49368d6fb5cf3112b9e0' 16 | , clientSecret: 'b65ad83daed242c0aa059ffae42feddd' 17 | } 18 | , foursquare: { 19 | clientId: 'VUGE4VHJMKWALKDKIOH1HLD1OQNHTC0PBZZBUQSHJ3WKW04K' 20 | , clientSecret: '0LVAGARGUN05DEDDRVWNIMH4RFIHEFV0CERU3OITAZW1CXGX' 21 | } 22 | , gowalla: { 23 | apiKey: '11cf666912004d709fa4bbf21718a82e', 24 | apiSecret: 'e1e23f135776452898a6d268129bf153' 25 | } 26 | , linkedin: { 27 | apiKey: 'pv6AWspODUeHIPNZfA531OYcFyB1v23u3y-KIADJdpyw54BXh-ciiQnduWf6FNRH' 28 | , apiSecret: 'Pdx7DCoJRdAk0ai3joXsslZvK1DPCQwsLn-T17Opkae22ZYDP5R7gmAoFes9TNHy' 29 | } 30 | , google: { 31 | clientId: '3335216477.apps.googleusercontent.com' 32 | , clientSecret: 'PJMW_uP39nogdu0WpBuqMhtB' 33 | } 34 | , yahoo: { 35 | consumerKey: 'dj0yJmk9RVExRlhPRE9rV1hSJmQ9WVdrOWEyRTBVMUJoTm1zbWNHbzlNVE13TURFeU9UTTJNZy0tJnM9Y29uc3VtZXJzZWNyZXQmeD1iYg--' 36 | , consumerSecret: 'efe6ae4982217630fe3aebf6e6fa1e82c02eba0b' 37 | } 38 | , readability: { 39 | consumerKey: 'Alfrednerstu' 40 | , consumerSecret: 'MXGftcxrRNMYn66CVmADR3KRnygCdYSk' 41 | } 42 | , justintv: { 43 | consumerKey: 'enter your consumer key here' 44 | , consumerSecret: 'enter your consumer secret here' 45 | } 46 | , '37signals': { 47 | clientId: 'cd4bf9cd9ed828b6bed8b67e6b314cf8b90c8de5' 48 | , clientSecret: '07883c36b4f4493b70f31872ed8fbdb099ff1cef' 49 | } 50 | , tumblr: { 51 | consumerKey: 'TAofjqRz9iKiAjtPMnXzHELIeQAw8cqKCZVXaEFSAxBrrvV99f' 52 | , consumerSecret: 's8ldFtirtsnWGSiBjwpUwMct8Yh4sliS9Uiocqsv3bw0ovMtlR' 53 | } 54 | , dropbox: { 55 | consumerKey: 'uhfqnbely5stdtm' 56 | , consumerSecret: 'jr7ofuwo32l7vkd' 57 | } 58 | , vimeo: { 59 | consumerKey: 'Enter your consumer key here' 60 | , consumerSecret: 'Enter your consumer secret here' 61 | } 62 | , box: { 63 | apiKey: '5hl66lbfy0quj8qhhzcn57dflb55y4rg' 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /lib/modules/googlehybrid.js: -------------------------------------------------------------------------------- 1 | var openidModule = require('./openid') 2 | , OAuth = require('oauth').OAuth 3 | , oid = require('openid') 4 | , extractHostname = require('../utils').extractHostname; 5 | 6 | var googlehybrid = module.exports = 7 | openidModule.submodule('googlehybrid') 8 | .configurable({ 9 | scope: 'array of desired google api scopes' 10 | , consumerKey: 'consumerKey' 11 | , consumerSecret: 'consumerSecret' 12 | }) 13 | .definit( function () { 14 | this.relyingParty = new oid.RelyingParty(this.myHostname() + this.callbackPath(), null, false, false, [ 15 | new oid.AttributeExchange({ 16 | "http://axschema.org/contact/email": "required", 17 | "http://axschema.org/namePerson/first": "required", 18 | "http://axschema.org/namePerson/last": "required" 19 | }), 20 | new oid.OAuthHybrid({ 21 | 'consumerKey' : this.consumerKey(), 22 | 'scope' : this.scope().join('+') 23 | }) 24 | ]); 25 | 26 | this.oa = new OAuth( 27 | "https://www.google.com/accounts/OAuthGetRequestToken", 28 | "https://www.google.com/accounts/OAuthGetAccessToken", 29 | this.consumerKey(), 30 | this.consumerSecret(), 31 | "1.0", null, "HMAC-SHA1"); 32 | }) 33 | .verifyAttributes(function(req,res) { 34 | var p = this.Promise() 35 | oa = this.oa; 36 | this.relyingParty.verifyAssertion(req, function (userAttributes) { 37 | if (userAttributes['authenticated']) { 38 | oa.getOAuthAccessToken(userAttributes['request_token'], undefined, function(error, oauth_access_token, oauth_access_token_secret) { 39 | userAttributes['access_token'] = oauth_access_token; 40 | userAttributes['access_token_secret'] = oauth_access_token_secret; 41 | p.fulfill(userAttributes) 42 | }); 43 | } else { 44 | p.fail(userAttributes['error']) 45 | } 46 | }); 47 | return p; 48 | }) 49 | .sendToAuthenticationUri(function(req,res) { 50 | 51 | // Automatic hostname detection + assignment 52 | if (!this._myHostname || this._alwaysDetectHostname) { 53 | this.myHostname(extractHostname(req)); 54 | } 55 | 56 | this.relyingParty.authenticate('http://www.google.com/accounts/o8/id', false, function(authenticationUrl){ 57 | if(authenticationUrl) { 58 | res.writeHead(302, { Location: authenticationUrl }); 59 | res.end(); 60 | } 61 | }); 62 | }) 63 | .entryPath('/auth/googlehybrid') 64 | .callbackPath('/auth/googlehybrid/callback'); 65 | -------------------------------------------------------------------------------- /test/password.test.js: -------------------------------------------------------------------------------- 1 | var tobi = require('tobi') 2 | , should = require('should') 3 | , browser = tobi.createBrowser(3000, 'local.host'); 4 | 5 | // Test a successful registration 6 | browser.get('/register', function (res, $) { 7 | $('form') 8 | .fill({ email: 'newuser@example.com', password: 'pass' }) 9 | .submit( function (res, $) { 10 | res.should.have.status(200); 11 | $('h2').should.have.text('Authenticated'); 12 | $('h2').should.not.have.text('Not Authenticated'); 13 | }); 14 | }); 15 | 16 | // Test failed registrations 17 | browser.get('/register', function (res, $) { 18 | $('form') 19 | .fill({ email: '', password: '' }) 20 | .submit( function (res, $) { 21 | res.should.have.status(200); 22 | $('#errors li:first').should.have.text('Missing email'); 23 | $('#errors li:eq(1)').should.have.text('Missing password'); 24 | }); 25 | $('form') 26 | .fill({ email: 'newuser', password: 'pass' }) 27 | .submit( function (res, $) { 28 | res.should.have.status(200); 29 | $('#errors').should.have.text('Please correct your email.'); 30 | }); 31 | $('form') 32 | .fill({ email: 'newuser', password: '' }) 33 | .submit( function (res, $) { 34 | res.should.have.status(200); 35 | $('#errors li:first').should.have.text('Please correct your email.'); 36 | $('#errors li:eq(1)').should.have.text('Missing password'); 37 | }); 38 | $('form') 39 | .fill({ email: 'abc@example.com', password: '' }) 40 | .submit( function (res, $) { 41 | res.should.have.status(200); 42 | $('#errors').should.have.text('Missing password'); 43 | }); 44 | 45 | // TODO Add case of person trying to take an existing login 46 | }); 47 | 48 | // Test a successful login 49 | browser.get('/login', function (res, $) { 50 | $('form') 51 | .fill({ email: 'brian@example.com', password: 'password' }) 52 | .submit( function (res, $) { 53 | res.should.have.status(200); 54 | $('h2').should.have.text('Authenticated'); 55 | $('h2').should.not.have.text('Not Authenticated'); 56 | }); 57 | }); 58 | 59 | // Test failed logins 60 | browser.get('/login', function (res, $) { 61 | $('form') 62 | .fill({ email: 'brian@example.com', password: 'wrongpassword' }) 63 | .submit( function (res, $) { 64 | res.should.have.status(200); 65 | $('#errors').should.have.text('Login failed'); 66 | }); 67 | $('form') 68 | .fill({ email: 'brian@example.com', password: '' }) 69 | .submit( function (res, $) { 70 | $('#errors').should.have.text('Missing password'); 71 | }); 72 | $('form') 73 | .fill({ email: '', password: '' }) 74 | .submit( function (res, $) { 75 | $('#errors li:first').should.have.text('Missing login'); 76 | $('#errors li:eq(1)').should.have.text('Missing password'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /lib/promise.js: -------------------------------------------------------------------------------- 1 | var Promise = function (values) { 2 | this._callbacks = []; 3 | this._errbacks = []; 4 | this._timebacks = []; 5 | if (arguments.length > 0) { 6 | this.fulfill.apply(this, values); 7 | } 8 | }; 9 | 10 | Promise.prototype = { 11 | callback: function (fn, scope) { 12 | if (this.values) { 13 | fn.apply(scope, this.values); 14 | return this; 15 | } 16 | this._callbacks.push([fn, scope]); 17 | return this; 18 | } 19 | , errback: function (fn, scope) { 20 | if (this.err) { 21 | fn.call(scope, this.err); 22 | return this; 23 | } 24 | this._errbacks.push([fn, scope]); 25 | return this; 26 | } 27 | , timeback: function (fn, scope) { 28 | if (this.timedOut) { 29 | fn.call(scope); 30 | return this; 31 | } 32 | this._timebacks.push([fn, scope]); 33 | return this; 34 | } 35 | , fulfill: function () { 36 | if (this.isFulfilled) return; 37 | this.isFulfilled = true; 38 | if (this._timeout) clearTimeout(this._timeout); 39 | var callbacks = this._callbacks; 40 | this.values = arguments; 41 | for (var i = 0, l = callbacks.length; i < l; i++) { 42 | callbacks[i][0].apply(callbacks[i][1], arguments); 43 | } 44 | return this; 45 | } 46 | , fail: function (err) { 47 | if (this._timeout) clearTimeout(this._timeout); 48 | var errbacks = this._errbacks; 49 | if ('string' === typeof err) 50 | err = new Error(err); 51 | this.err = err; 52 | for (var i = 0, l = errbacks.length; i < l; i++) { 53 | errbacks[i][0].call(errbacks[i][1], err); 54 | } 55 | return this; 56 | } 57 | , timeout: function (ms) { 58 | if (this.values || this.err) return this; 59 | var timebacks = this._timebacks 60 | , self = this; 61 | if (ms === -1) return this; 62 | this._timeout = setTimeout(function () { 63 | self.timedOut = true; 64 | for (var i = 0, l = timebacks.length; i < l; i++) { 65 | timebacks[i][0].call(timebacks[i][1]); 66 | } 67 | }, ms); 68 | return this; 69 | } 70 | }; 71 | 72 | var ModulePromise = module.exports = function (_module, values) { 73 | if (values) 74 | Promise.call(this, values); 75 | else 76 | Promise.call(this); 77 | this.module = _module; 78 | }; 79 | 80 | ModulePromise.prototype.__proto__ = Promise.prototype; 81 | 82 | ModulePromise.prototype.breakTo = function (seqName) { 83 | if (this._timeout) clearTimeout(this._timeout); 84 | 85 | var args = Array.prototype.slice.call(arguments, 1); 86 | var _module = this.module 87 | , seq = _module._stepSequences[seqName]; 88 | if (_module.everyauth.debug) 89 | console.log('breaking out to ' + seq.name); 90 | seq = seq.materialize(); 91 | seq.start.apply(seq, args); 92 | // TODO Garbage collect the abandoned sequence 93 | }; 94 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect') 2 | , __pause = connect.utils.pause 3 | , everyauth = module.exports = {}; 4 | 5 | // TODO Deprecate exposure of Promise 6 | everyauth.Promise = require('./lib/promise'); 7 | 8 | everyauth.helpExpress = require('./lib/expressHelper'); 9 | 10 | everyauth.debug = false; 11 | 12 | // The connect middleware. e.g., 13 | // connect( 14 | // ... 15 | // , everyauth.middleware() 16 | // , ... 17 | // ) 18 | everyauth.middleware = function () { 19 | var app = connect( 20 | function registerReqGettersAndMethods (req, res, next) { 21 | var methods = everyauth._req._methods 22 | , getters = everyauth._req._getters; 23 | for (var name in methods) { 24 | req[name] = methods[name]; 25 | } 26 | for (name in getters) { 27 | Object.defineProperty(req, name, { 28 | get: getters[name] 29 | }); 30 | } 31 | next(); 32 | } 33 | , function fetchUserFromSession (req, res, next) { 34 | var sess = req.session 35 | , auth = sess && sess.auth 36 | , everymodule, findUser; 37 | if (!auth) return next(); 38 | if (!auth.userId) return next(); 39 | everymodule = everyauth.everymodule; 40 | if (!everymodule._findUserById) return next(); 41 | var pause = __pause(req); 42 | everymodule._findUserById(auth.userId, function (err, user) { 43 | if (err) throw err; // TODO Leverage everyauth's error handling 44 | if (user) req.user = user; 45 | else delete sess.auth; 46 | next(); 47 | pause.resume(); 48 | }); 49 | } 50 | , connect.router(function (app) { 51 | var modules = everyauth.enabled 52 | , _module; 53 | for (var _name in modules) { 54 | _module = modules[_name]; 55 | _module.validateSteps(); 56 | _module.routeApp(app); 57 | } 58 | }) 59 | ); 60 | return app; 61 | }; 62 | 63 | everyauth._req = { 64 | _methods: {} 65 | , _getters: {} 66 | }; 67 | 68 | everyauth.addRequestMethod = function (name, fn) { 69 | this._req._methods[name] = fn; 70 | return this; 71 | }; 72 | 73 | everyauth.addRequestGetter = function (name, fn, isAsync) { 74 | this._req._getters[name] = fn; 75 | return this; 76 | }; 77 | 78 | everyauth 79 | .addRequestMethod('logout', function () { 80 | var req = this; 81 | delete req.session.auth; 82 | 83 | }).addRequestGetter('loggedIn', function () { 84 | var req = this; 85 | if (req.session.auth && req.session.auth.loggedIn) { 86 | return true; 87 | } else { 88 | return false; 89 | } 90 | }); 91 | 92 | everyauth.modules = {}; 93 | everyauth.enabled = {}; 94 | 95 | // Grab all filenames in ./modules -- They correspond to the modules of the same name 96 | // as the filename (minus '.js') 97 | var fs = require('fs'); 98 | var files = fs.readdirSync(__dirname + '/lib/modules'); 99 | var includeModules = files.map( function (fname) { 100 | return fname.substring(0, fname.length - 3); 101 | }); 102 | for (var i = 0, l = includeModules.length; i < l; i++) { 103 | var name = includeModules[i]; 104 | 105 | // Lazy enabling of a module via `everyauth` getters 106 | // i.e., the `facebook` module is not loaded into memory 107 | // until `everyauth.facebook` is evaluated 108 | Object.defineProperty(everyauth, name, { 109 | get: (function (name) { 110 | return function () { 111 | var mod = this.modules[name] || (this.modules[name] = require('./lib/modules/' + name)); 112 | // Make `everyauth` accessible from each auth strategy module 113 | if (!mod.everyauth) mod.everyauth = this; 114 | if (mod.shouldSetup) 115 | this.enabled[name] = mod; 116 | return mod; 117 | } 118 | })(name) 119 | }); 120 | }; 121 | -------------------------------------------------------------------------------- /lib/modules/openid.js: -------------------------------------------------------------------------------- 1 | var everyModule = require('./everymodule') 2 | , oid = require('openid') 3 | , url = require('url') 4 | , extractHostname = require('../utils').extractHostname; 5 | 6 | var openid = module.exports = 7 | everyModule.submodule('openid') 8 | .configurable({ 9 | simpleRegistration: 'e.g., {"nickname" : true}' 10 | , attributeExchange: 'eg {"http://axschema.org/contact/email": "required"}' 11 | , myHostname: 'e.g., http://localhost:3000 . Notice no trailing slash' 12 | , alwaysDetectHostname: 'does not cache myHostname once. Instead, re-detect it on every request. Good for multiple subdomain architectures' 13 | , redirectPath : 'The path to redirect To' 14 | , openidURLField : 'The post field to use for open id' 15 | }) 16 | .definit( function () { 17 | this.relyingParty = new oid.RelyingParty(this.myHostname() + this.callbackPath(), null, false, false, [ 18 | new oid.UserInterface() 19 | , new oid.SimpleRegistration(this.simpleRegistration()) 20 | , new oid.AttributeExchange(this.attributeExchange()) 21 | ]); 22 | }) 23 | .get('entryPath', 24 | 'the link a user follows, whereupon you kick off the OpenId auth process - e.g., "/auth/openid"') 25 | .step('sendToAuthenticationUri') 26 | .description('sends the user to the providers openid authUrl') 27 | .accepts('req res') 28 | .promises(null) 29 | .get('callbackPath', 30 | 'the callback path that the 3rd party Openid provider redirects to after an authorization result - e.g., "/auth/openid/callback"') 31 | .step('verifyAttributes') 32 | .description('verifies the return attributes') 33 | .accepts('req res') 34 | .promises('userAttributes') 35 | .step('getSession') 36 | .accepts('req') 37 | .promises('session') 38 | .step('findOrCreateUser') 39 | .accepts('session userAttributes') 40 | .promises('user') 41 | .step('addToSession') 42 | .accepts('session user') 43 | .promises(null) 44 | .step('sendResponse') 45 | .accepts('res') 46 | .promises(null) 47 | .sendToAuthenticationUri(function(req,res) { 48 | 49 | // Automatic hostname detection + assignment 50 | if (!this._myHostname || this._alwaysDetectHostname) { 51 | this.myHostname(extractHostname(req)); 52 | } 53 | 54 | this.relyingParty.authenticate(req.query[this.openidURLField()], false, function(authenticationUrl){ 55 | if(authenticationUrl) { 56 | res.writeHead(302, { Location: authenticationUrl }); 57 | res.end(); 58 | } 59 | }); 60 | }) 61 | .getSession( function(req) { 62 | return req.session; 63 | }) 64 | .verifyAttributes(function(req,res) { 65 | var p = this.Promise(); 66 | this.relyingParty.verifyAssertion(req, function (userAttributes) { 67 | if(!userAttributes.authenticated) return p.fail([userAttributes.error]); 68 | 69 | p.fulfill(userAttributes) 70 | }); 71 | return p; 72 | }) 73 | .addToSession( function (sess, user) { 74 | var _auth = sess.auth || (sess.auth = {}) 75 | , mod = _auth[this.name] || (_auth[this.name] = {}); 76 | _auth.loggedIn = true; 77 | _auth.userId = user.id; 78 | mod.user = user; 79 | }) 80 | .sendResponse( function (res) { 81 | var redirectTo = this.redirectPath(); 82 | if (!redirectTo) 83 | throw new Error('You must configure a redirectPath'); 84 | res.writeHead(303, {'Location': redirectTo}); 85 | res.end(); 86 | }) 87 | .redirectPath('/') 88 | .entryPath('/auth/openid') 89 | .callbackPath('/auth/openid/callback') 90 | .simpleRegistration({ 91 | "nickname" : true 92 | , "email" : true 93 | , "fullname" : true 94 | , "dob" : true 95 | , "gender" : true 96 | , "postcode" : true 97 | , "country" : true 98 | , "language" : true 99 | , "timezone" : true 100 | }) 101 | .attributeExchange({ 102 | "http://axschema.org/contact/email" : "required" 103 | , "http://axschema.org/namePerson/friendly" : "required" 104 | , "http://axschema.org/namePerson" : "required" 105 | , "http://axschema.org/namePerson/first" : "required" 106 | , "http://axschema.org/contact/country/home": "required" 107 | , "http://axschema.org/media/image/default" : "required" 108 | , "http://axschema.org/x/media/signature" : "required" 109 | }) 110 | .openidURLField('openid_identifier'); 111 | -------------------------------------------------------------------------------- /lib/modules/box.js: -------------------------------------------------------------------------------- 1 | var everyModule = require('./everymodule') 2 | , rest = require('../restler') 3 | , url = require('url'); 4 | 5 | require('xml2js'); 6 | 7 | var box = module.exports = 8 | everyModule.submodule('box') 9 | .configurable({ 10 | apiKey: 'The API key obtained when registering a project with OpenBox' 11 | , redirectPath: 'Where to redirect to after a failed or successful auth' 12 | }) 13 | 14 | .get('entryPath', 15 | 'the link a user follows, whereupon you redirect them to Box.net -- e.g., "/auth/box"') 16 | .step('getTicket') 17 | .description('asks Box.net for a unique ticket for each user of your app') 18 | .accepts('req res') 19 | .promises('ticket') 20 | .step('redirectToBoxAuth') 21 | .description('redirects the user to https://www.box.net/api/1.0/auth/') 22 | .accepts('res ticket') 23 | .promises(null) 24 | .get('callbackPath', 25 | 'the callback path that Box.net redirects to after an authorization result - e.g., "/auth/box/callback"') 26 | .step('extractAuthToken') 27 | .description('extracts auth_token from the url that Box.net redirects to after authorization') 28 | .accepts('req res') 29 | .promises('authToken') 30 | .step('getSession') 31 | .description('extracts the session from the incoming request') 32 | .accepts('req') 33 | .promises('session') 34 | .step('fetchUser') 35 | .description('fetches the authorizing user via the API with the authToken') 36 | .accepts('authToken') 37 | .promises('boxUser') 38 | .step('findOrCreateUser') 39 | //.optional() 40 | .accepts('session authToken boxUser') 41 | .promises('user') 42 | .step('compileAuth') 43 | .description('combines the ticket, auth token, fetched boxUser, and your app user into a single object') 44 | .accepts('authToken boxUser user') 45 | .promises('auth') 46 | .step('addToSession') 47 | .description('adds the auth token and box.net user metadata to the session') 48 | .accepts('session auth') 49 | .promises(null) 50 | .step('sendResponse') 51 | .description('sends a response to the client by rendering a successful auth view') 52 | .accepts('res') 53 | .promises(null) 54 | 55 | .getTicket( function (req, res) { 56 | var promise = this.Promise(); 57 | rest.get( this._apiHost + '/rest', { 58 | parser: rest.parsers.xml 59 | , query: { 60 | action: 'get_ticket' 61 | , api_key: this._apiKey 62 | } 63 | }).on('success', function (data, res) { 64 | var status = data.status 65 | , ticket = data.ticket; 66 | promise.fulfill(ticket); 67 | }).on('error', function (data, res) { 68 | promise.fail(data); 69 | }); 70 | return promise; 71 | }) 72 | .redirectToBoxAuth( function (res, ticket) { 73 | res.writeHead(303, { 74 | 'Location': this._apiHost + '/auth/' + ticket 75 | }); 76 | res.end(); 77 | }) 78 | 79 | .extractAuthToken( function (req, res) { 80 | var parsedUrl = url.parse(req.url, true) 81 | , authToken = parsedUrl.query && parsedUrl.query.auth_token; 82 | return authToken; 83 | }) 84 | .getSession( function (req, res) { 85 | return req.session; 86 | }) 87 | .fetchUser( function (authToken) { 88 | var promise = this.Promise(); 89 | rest.get(this._apiHost + '/rest', { 90 | parser: rest.parsers.xml 91 | , query: { 92 | action: 'get_account_info' 93 | , api_key: this._apiKey 94 | , auth_token: authToken 95 | } 96 | }).on('success', function (data, res) { 97 | var status = data.status 98 | , user = data.user; 99 | promise.fulfill(user); 100 | }).on('fail', function (data, res) { 101 | promise.fail(data); 102 | }); 103 | return promise; 104 | }) 105 | .compileAuth( function (authToken, boxUser, user) { 106 | return compiled = { 107 | authToken: authToken 108 | , boxUser: boxUser 109 | , user: user 110 | }; 111 | }) 112 | .addToSession( function (sess, auth) { 113 | var _auth = sess.auth || (sess.auth = {}) 114 | , mod = _auth[this.name] || (_auth[this.name] = {}); 115 | _auth.loggedIn = true; 116 | _auth.userId || (_auth.userId = auth.user.user_id); 117 | mod.user = auth.boxUser; 118 | mod.authToken = auth.authToken; 119 | }) 120 | .sendResponse( function (res) { 121 | var redirectTo = this.redirectPath(); 122 | if (!redirectTo) 123 | throw new Error('You must configure a redirectPath'); 124 | res.writeHead(303, {'Location': redirectTo}); 125 | res.end(); 126 | }) 127 | 128 | .entryPath('/auth/box') 129 | .callbackPath('/auth/box/callback'); 130 | 131 | box._apiHost = 'https://www.box.net/api/1.0'; 132 | -------------------------------------------------------------------------------- /example/views/home.jade: -------------------------------------------------------------------------------- 1 | - if (!everyauth.loggedIn) 2 | h2 Not Authenticated 3 | #register 4 | a(href='/register') Register 5 | #password-login 6 | a(href='/login', style='border: 0px') Login with Password 7 | #fb-login.fb_button(style='background-position: left -188px') 8 | a.fb_button_medium(href='/auth/facebook') 9 | span#fb_login_text.fb_button_text 10 | Connect with Facebook 11 | #twitter-login 12 | a(href='/auth/twitter', style='border: 0px') 13 | img(style='border: 0px', src='https://si0.twimg.com/images/dev/buttons/sign-in-with-twitter-l.png') 14 | #github-login 15 | a(href='/auth/github', style='border: 0px') 16 | img(style='border: 0px', src='http://github.com/intridea/authbuttons/raw/master/png/github_64.png') 17 | #instagram-login 18 | a(href='/auth/instagram', style='border: 0px') 19 | img(style='border: 0px', src='https://instagram.com/static/images/headerWithTitle.png') 20 | #foursquare-login 21 | a(href='/auth/foursquare', style='border: 0px') 22 | img(style='border: 0px', src='https://foursquare.com/img/headerLogo.png') 23 | #gowalla-login 24 | a(href='/auth/gowalla', style='border: 0px') 25 | img(style='border: 0px', src='http://static.gowalla.com/gowalla-connect-buttons/button-gowalla_connect-156ool.png') 26 | #linkedin-login 27 | a(href='/auth/linkedin', style='border: 0px') 28 | img(style='border: 0px', src='http://press.linkedin.com/sites/all/themes/presslinkedin/images/LinkedIn_WebLogo_LowResExample.jpg') 29 | #google-login 30 | a(href='/auth/google', style='border: 0px') 31 | img(style='border: 0px', src='https://www.google.com/favicon.ico') 32 | #yahoo-login 33 | a(href='/auth/yahoo', style='border: 0px') 34 | img(style='border: 0px', src='http://l.yimg.com/a/i/reg/openid/buttons/1_new.png') 35 | #googlehybrid-login 36 | a(href='/auth/googlehybrid', style='border: 0px') 37 | img(style='border: 0px', src='https://www.google.com/favicon.ico') 38 | | Hybrid 39 | #readability-login 40 | a(href='/auth/readability', style='border: 0px') 41 | img(style='border: 0px', src='https://www.readability.com/media/images/logo_chair.png') 42 | #dropbox-login 43 | a(href='/auth/dropbox', style='border: 0px') 44 | img(src='https://www.dropbox.com/static/16890/images/logo.png') 45 | #vimeo-login 46 | a(href='/auth/vimeo', style='border: 0px') 47 | img(src='http://a.vimeocdn.com/images/logo_vimeo.png') 48 | #justintv-login 49 | a(href='/auth/justintv', style='border: 0px') 50 | img(src='http://s.jtvnw.net/jtv_user_pictures/hosted_images/new_logo_148_40_black.png') 51 | #37signals-login 52 | a(href='/auth/37signals', style='border: 0px') 53 | img(src='http://37signals.com/svn/images/37slogo-trans.gif') 54 | #tumblr-login 55 | a(href='/auth/tumblr', style='border: 0px') 56 | img(src='http://assets.tumblr.com/images/logo.png?alpha&6') 57 | #box-login 58 | a(href='/auth/box', style='border: 0px') 59 | img(src='http://sites.box.net/apps/web/simpleshare/img/logo.png') 60 | #openid-login 61 | form#openid(action='/auth/openid') 62 | label(for='openid_identifier') OpenID Identifier:   63 | input(type='text', name='openid_identifier') 64 | input(type='submit') Login 65 | - else 66 | h2 Authenticated 67 | - if (everyauth.facebook) 68 | h3 Facebook User Data 69 | p= JSON.stringify(everyauth.facebook.user) 70 | - if (everyauth.twitter) 71 | h3 Twitter User Data 72 | p= JSON.stringify(everyauth.twitter.user) 73 | - if (everyauth.github) 74 | h3 GitHub User Data 75 | p= JSON.stringify(everyauth.github.user) 76 | - if (everyauth.instagram) 77 | h3 Instagram User Data 78 | p= JSON.stringify(everyauth.instagram.user) 79 | - if (everyauth.foursquare) 80 | h3 Foursquare User Data 81 | p= JSON.stringify(everyauth.foursquare.user) 82 | - if (everyauth.gowalla) 83 | h3 Gowalla User Data 84 | p= JSON.stringify(everyauth.gowalla.user) 85 | - if (everyauth.linkedin) 86 | h3 LinkedIn User Data 87 | p= JSON.stringify(everyauth.linkedin.user) 88 | - if (everyauth.google) 89 | h3 Google User Data 90 | p= JSON.stringify(everyauth.google.user) 91 | - if (everyauth.yahoo) 92 | h3 Yahoo User Data 93 | p= JSON.stringify(everyauth.yahoo.user) 94 | - if (everyauth.readability) 95 | h3 Readability User Data 96 | p= JSON.stringify(everyauth.readability.user) 97 | - if (everyauth.dropbox) 98 | h3 Dropbox User Data 99 | p= JSON.stringify(everyauth.dropbox.user) 100 | - if (everyauth.vimeo) 101 | h3 Vimeo User Data 102 | p= JSON.stringify(everyauth.vimeo.user) 103 | - if (everyauth.justintv) 104 | h3 Justin.tv User Data 105 | p= JSON.stringify(everyauth.justintv.user) 106 | - if (everyauth['37signals']) 107 | h3 37signals User Data 108 | p= JSON.stringify(everyauth['37signals'].user) 109 | - if (everyauth.tumblr) 110 | h3 Tumblr User Data 111 | p= JSON.stringify(everyauth.tumblr.user) 112 | - if (everyauth.box) 113 | h3 Box.net User Data 114 | p= JSON.stringify(everyauth.box.user) 115 | - if (everyauth.openid) 116 | h3 Openid User Data 117 | p= JSON.stringify(everyauth.openid.user) 118 | 119 | h3 120 | a(href='/logout') Logout 121 | -------------------------------------------------------------------------------- /lib/step.js: -------------------------------------------------------------------------------- 1 | var Promise = require('./promise') 2 | , clone = require('./utils').clone; 3 | 4 | var Step = module.exports = function Step (name, _module) { 5 | this.name = name; 6 | 7 | // defineProperty; otherwise, 8 | // clone will overflow when we clone a module 9 | Object.defineProperty(this, 'module', { 10 | value: _module 11 | }); 12 | }; 13 | 14 | Step.prototype = { 15 | /** 16 | * @returns {Promise} 17 | */ 18 | exec: function (seq) { 19 | var accepts = this.accepts 20 | , promises = this.promises 21 | , block = this.block 22 | , _module = this.module 23 | , errorCallback = this.errback || _module._moduleErrback // Configured errback 24 | , self = this; 25 | 26 | if (this.debug) 27 | console.log('starting step - ' + this.name); 28 | 29 | var args = this._unwrapArgs(seq); 30 | 31 | // There is a hidden last argument to every step function that 32 | // is all the data promised by prior steps up to the step's point 33 | // in time. We cannot anticipate everything a developer may want via 34 | // `accepts(...)`. Therefore, just in case, we give the developer 35 | // access to all data promised by prior steps via the last argument -- `seq.values` 36 | args.push(seq.values); 37 | 38 | try { 39 | // Apply the step logic 40 | 41 | // Add _super access 42 | _module._super = function () { 43 | var step = this.__proto__._steps[self.name]; 44 | if (!step) return; 45 | var superArgs = arguments.length ? arguments : args; 46 | step.block.apply(this, superArgs); 47 | }; 48 | ret = block.apply(_module, args); 49 | delete _module._super; 50 | } catch (breakTo) { 51 | // Catch any sync breakTo's if any 52 | if (breakTo.isSeq) { 53 | console.log("breaking out to " + breakTo.name); 54 | breakTo.start.apply(breakTo, breakTo.initialArgs); 55 | // TODO Garbage collect the promise chain 56 | return; 57 | } else { 58 | // Else, we have a regular exception 59 | // TODO Scope this fn 60 | errorCallback(breakTo, seq.values); 61 | } 62 | } 63 | 64 | if (promises && promises.length && 65 | 'undefined' === typeof ret) { 66 | // TODO Scope this fn 67 | errorCallback( 68 | new Error('Step ' + this.name + ' of `' + _module.name + 69 | '` is promising: ' + promises.join(', ') + 70 | ' ; however, the step returns nothing. ' + 71 | 'Fix the step by returning the expected values OR ' + 72 | 'by returning a Promise that promises said values.') 73 | ); 74 | } 75 | // Convert return value into a Promise 76 | // if it's not yet a Promise 77 | ret = (ret instanceof Promise) 78 | ? ret 79 | : Array.isArray(ret) 80 | ? promises.length === 1 81 | ? this.module.Promise([ret]) 82 | : this.module.Promise(ret) 83 | : this.module.Promise([ret]); 84 | 85 | ret.callback( function () { 86 | if (seq.debug) 87 | console.log('...finished step'); 88 | }); 89 | 90 | var convertErr = _module._convertErr; 91 | if (convertErr) { 92 | var oldErrback = ret.errback; 93 | ret.errback = function (fn, scope) { 94 | var oldFn = fn; 95 | fn = function (err) { 96 | if (err.constructor === Object) { 97 | err = convertErr(err); 98 | } else if ('string' === typeof err) { 99 | err = new Error(err); 100 | } 101 | return oldFn.call(this, err); 102 | }; 103 | return oldErrback.call(this, fn, scope); 104 | }; 105 | } 106 | 107 | // TODO Scope this fn -- i.e., errorCallback? 108 | ret.errback(errorCallback); 109 | 110 | ret.callback( function () { 111 | // Store the returned values 112 | // in the sequence's state via seq.values 113 | var returned = arguments 114 | , vals = seq.values; 115 | if (promises !== null) promises.forEach( function (valName, i) { 116 | vals[valName] = returned[i]; 117 | }); 118 | }); 119 | 120 | ret.timeback( function () { 121 | ret.fail(new Error('Step ' + self.name + ' of `' + _module.name + '` module timed out.')); 122 | }); 123 | 124 | var timeoutMillis = this.timeout || 125 | _module.moduleTimeout(); 126 | ret.timeout(timeoutMillis); 127 | 128 | return ret; 129 | } 130 | /** 131 | * Unwraps values (from the sequence) based on 132 | * the step's this.accepts spec. 133 | */ 134 | , _unwrapArgs: function (seq) { 135 | return this.accepts.reduce( function (args, accept) { 136 | args.push(seq.values[accept]); 137 | return args; 138 | }, []); 139 | } 140 | , clone: function (name, _module) { 141 | var step = new Step(name, _module); 142 | step.accepts = clone(this.accepts); 143 | step.promises = clone(this.promises); 144 | step.description = this.description; 145 | step.timeout = this.timeout; 146 | step.errback = this.errback; 147 | return step; 148 | } 149 | }; 150 | 151 | Object.defineProperty(Step.prototype, 'block', { 152 | get: function () { 153 | return this._block || (this._block = this.module[this.name]()); 154 | } 155 | }); 156 | 157 | Object.defineProperty(Step.prototype, 'debug', { 158 | get: function () { 159 | return this.module.everyauth.debug; 160 | } 161 | }); 162 | -------------------------------------------------------------------------------- /lib/stepSequence.js: -------------------------------------------------------------------------------- 1 | var Promise = require('./promise') 2 | , clone = require('./utils').clone; 3 | 4 | var materializedMethods = { 5 | isSeq: true 6 | /** 7 | * Sets up the immediate or eventual 8 | * output of priorPromise to pipe to 9 | * the nextStep's promise 10 | * @param {Promise} priorPromise 11 | * @param {Step} nextStep 12 | * @returns {Promise} 13 | */ 14 | , _bind: function (priorPromise, nextStep) { 15 | var nextPromise = this.module.Promise() 16 | , seq = this; 17 | 18 | priorPromise.callback( function () { 19 | var resultPromise = nextStep.exec(seq); 20 | if (!resultPromise) return; // if we have a breakTo 21 | resultPromise.callback( function () { 22 | nextPromise.fulfill(); 23 | }); // TODO breakback? 24 | }); 25 | return nextPromise; 26 | } 27 | 28 | /** 29 | * This kicks off a sequence of steps. 30 | * Creates a new chain of promises and exposes the leading promise 31 | * to the incoming (req, res) pair from the route handler 32 | */ 33 | , start: function () { 34 | var steps = this.steps; 35 | 36 | this._transposeArgs(arguments); 37 | 38 | // Pipe through the steps 39 | var priorStepPromise = steps[0].exec(this); 40 | 41 | // If we have a breakTo 42 | if (!priorStepPromise) return; 43 | 44 | for (var i = 1, l = steps.length; i < l; i++) { 45 | priorStepPromise = this._bind(priorStepPromise, steps[i]); 46 | } 47 | return priorStepPromise; 48 | } 49 | 50 | /** 51 | * Used for exposing the leading promise 52 | * of a step promise chain to the incoming 53 | * args (e.g., [req, res] pair from the 54 | * route handler) 55 | */ 56 | , _transposeArgs: function (args) { 57 | var firstStep = this.steps[0] 58 | , seq = this; 59 | firstStep.accepts.forEach( function (paramName, i) { 60 | // Map the incoming arguments to the named parameters 61 | // that the first step is expected to accept. 62 | seq.values[paramName] = args[i]; 63 | }); 64 | } 65 | }; 66 | 67 | var StepSequence = module.exports = function StepSequence (name, _module) { 68 | this.name = name; 69 | this.module = _module; 70 | this.orderedStepNames = []; 71 | } 72 | 73 | StepSequence.prototype = { 74 | constructor: StepSequence 75 | , clone: function (submodule) { 76 | var ret = new (this.constructor)(this.name, submodule); 77 | ret.orderedStepNames = clone(this.orderedStepNames); 78 | return ret; 79 | } 80 | 81 | , materialize: function () { 82 | var ret = Object.create(this); 83 | ret.values = {}; 84 | for (var k in materializedMethods) { 85 | ret[k] = materializedMethods[k]; 86 | } 87 | return ret; 88 | } 89 | 90 | // TODO Replace logic here with newer introspection code 91 | , checkSteps: function () { 92 | var steps = this.steps 93 | , step 94 | , paramsToDate = [] 95 | , missingParams; 96 | for (var i = 0, l = steps.length; i < l; i++) { 97 | step = steps[i]; 98 | if ('undefined' === typeof step.accepts) 99 | throw new Error('You did not declare accepts for the step: ' + step.name); 100 | if ('undefined' === typeof step.promises) { 101 | throw new Error('You did not declare promises for the step: ' + step.name); 102 | } 103 | 104 | if (i === 0) 105 | paramsToDate = paramsToDate.concat(step.accepts); 106 | 107 | missingParams = step.accepts.filter( function (param) { 108 | return paramsToDate.indexOf(param) === -1; 109 | }); 110 | 111 | if (i > 0 && missingParams.length) 112 | throw new Error('At step, ' + step.name + ', you are trying to access the parameters: ' + missingParams.join(', ') + ' . However, only the following parameters have been promised from prior steps thus far: ' + paramsToDate.join(', ')); 113 | 114 | paramsToDate = paramsToDate.concat(step.promises); 115 | 116 | if ('undefined' === typeof this.module[step.name]()) 117 | // TODO Remove this Error, since invoking the arg to typeof (see line above) 118 | // already throws an Error 119 | throw new Error('No one defined the function for the following step: ' + step.name + ' in the module ' + this.module.name); 120 | } 121 | } 122 | }; 123 | 124 | Object.defineProperty(StepSequence.prototype, 'steps', { 125 | get: function () { 126 | // Compile the steps by pulling the step names 127 | // from the module 128 | var allSteps = this.module._steps 129 | , orderedSteps = this.orderedStepNames.map( function (stepName) { 130 | return allSteps[stepName]; 131 | }) 132 | , seq = this; 133 | 134 | function compileSteps () { 135 | var ret 136 | , paramsToDate = [] 137 | , missingParams; 138 | 139 | ret = orderedSteps.map( function (step, i) { 140 | var meta = { missing: [], step: step, missingParams: [], paramsToDate: {} }; 141 | if (! ('promises' in step)) { 142 | meta.missing.push('promises'); 143 | } 144 | if (! ('accepts' in step)) { 145 | meta.missing.push('accepts'); 146 | } 147 | 148 | if (('accepts' in step) && i === 0) 149 | paramsToDate = paramsToDate.concat(step.accepts); 150 | 151 | missingParams = !step.accepts ? [] : step.accepts.filter( function (param) { 152 | return paramsToDate.indexOf(param) === -1; 153 | }); 154 | 155 | if (step.promises) 156 | paramsToDate = paramsToDate.concat(step.promises); 157 | 158 | if (missingParams.length) { 159 | meta.missingParams = missingParams; 160 | meta.paramsToDate = paramsToDate; 161 | } 162 | 163 | if (! (('_' + step.name) in seq.module)) 164 | meta.missing.push('its function'); 165 | 166 | return meta; 167 | 168 | }); 169 | 170 | return ret; 171 | } 172 | 173 | var compiledSteps; 174 | 175 | Object.defineProperty(orderedSteps, 'incomplete', { get: function () { 176 | compiledSteps || (compiledSteps = compileSteps()); 177 | return compiledSteps.filter( function (stepMeta) { 178 | return stepMeta.missing.length > 0; 179 | }).map( function (stepMeta) { 180 | var error = 'is missing: ' + 181 | stepMeta.missing.join(', '); 182 | return { name: stepMeta.step.name, error: error }; 183 | }); 184 | } }); 185 | 186 | Object.defineProperty(orderedSteps, 'invalid', { get: function () { 187 | compiledSteps || (compiledSteps = compileSteps()); 188 | return compiledSteps.filter( function (stepMeta) { 189 | return stepMeta.missingParams.length > 0; 190 | }).map( function (stepMeta) { 191 | var error = 'is trying to accept the parameters: ' + 192 | stepMeta.missingParams.join(', ') + 193 | ' . However, only the following parameters have ' + 194 | 'been promised from prior steps thus far: ' + 195 | stepMeta.paramsToDate.join(', '); 196 | return { name: stepMeta.step.name, error: error }; 197 | }); 198 | } }); 199 | 200 | return orderedSteps; 201 | } 202 | }); 203 | 204 | Object.defineProperty(StepSequence.prototype, 'debug', { 205 | get: function () { 206 | return this.module.everyauth.debug; 207 | } 208 | }); 209 | -------------------------------------------------------------------------------- /lib/modules/oauth2.js: -------------------------------------------------------------------------------- 1 | var everyModule = require('./everymodule') 2 | , OAuth = require('oauth').OAuth2 3 | , url = require('url') 4 | , querystring = require('querystring') 5 | , rest = require('../restler') 6 | , extractHostname = require('../utils').extractHostname; 7 | 8 | // Steps define a sequence of logic that pipes data through 9 | // a chain of promises. A new chain of promises is generated 10 | // every time the set of steps is started. 11 | 12 | var oauth2 = module.exports = 13 | everyModule.submodule('oauth2') 14 | .definit( function () { 15 | this.oauth = new OAuth(this.appId(), this.appSecret(), this.oauthHost(), this.authPath(), this.accessTokenPath()); 16 | }) 17 | .configurable({ 18 | apiHost: 'e.g., https://graph.facebook.com' 19 | , oauthHost: 'the host for the OAuth provider' 20 | , appId: 'the OAuth app id provided by the host' 21 | , appSecret: 'the OAuth secret provided by the host' 22 | , authPath: "the path on the OAuth provider's domain where " + 23 | "we direct the user for authentication, e.g., /oauth/authorize" 24 | , accessTokenPath: "the path on the OAuth provider's domain " + 25 | "where we request the access token, e.g., /oauth/access_token" 26 | , accessTokenHttpMethod: 'the http method ("get" or "post") with which to make our access token request' 27 | , postAccessTokenParamsVia: '"query" to POST the params to the access ' + 28 | 'token endpoint as a querysting; "data" to POST the params to ' + 29 | 'the access token endpoint in the request body' 30 | , myHostname: 'e.g., http://local.host:3000 . Notice no trailing slash' 31 | , alwaysDetectHostname: 'does not cache myHostname once. Instead, re-detect it on every request. Good for multiple subdomain architectures' 32 | , redirectPath: 'Where to redirect to after a failed or successful OAuth authorization' 33 | , convertErr: 'a function (data) that extracts an error message from data arg, where `data` is what is returned from a failed OAuth request' 34 | , authCallbackDidErr: 'Define the condition for the auth module determining if the auth callback url denotes a failure. Returns true/false.' 35 | }) 36 | 37 | // Declares a GET route that is aliased 38 | // as 'entryPath'. The handler for this route 39 | // triggers the series of steps that you see 40 | // indented below it. 41 | .get('entryPath', 42 | 'the link a user follows, whereupon you redirect them to the 3rd party OAuth provider dialog - e.g., "/auth/facebook"') 43 | .step('getAuthUri') 44 | .accepts('req res') 45 | .promises('authUri') 46 | .step('requestAuthUri') 47 | .accepts('res authUri') 48 | .promises(null) 49 | 50 | .get('callbackPath', 51 | 'the callback path that the 3rd party OAuth provider redirects to after an OAuth authorization result - e.g., "/auth/facebook/callback"') 52 | .step('getCode') 53 | .description('retrieves a verifier code from the url query') 54 | .accepts('req res') 55 | .promises('code') 56 | .canBreakTo('authCallbackErrorSteps') 57 | .step('getAccessToken') 58 | .accepts('code') 59 | .promises('accessToken extra') 60 | .step('fetchOAuthUser') 61 | .accepts('accessToken') 62 | .promises('oauthUser') 63 | .step('getSession') 64 | .accepts('req') 65 | .promises('session') 66 | .step('findOrCreateUser') 67 | //.optional() 68 | .accepts('session accessToken extra oauthUser') 69 | .promises('user') 70 | .step('compile') 71 | .accepts('accessToken extra oauthUser user') 72 | .promises('auth') 73 | .step('addToSession') 74 | .accepts('session auth') 75 | .promises(null) 76 | .step('sendResponse') 77 | .accepts('res') 78 | .promises(null) 79 | 80 | .stepseq('authCallbackErrorSteps') 81 | .step('handleAuthCallbackError', 82 | 'a request handler that intercepts a failed authorization message sent from the OAuth2 provider to your service. e.g., the request handler for "/auth/facebook/callback?error_reason=user_denied&error=access_denied&error_description=The+user+denied+your+request."') 83 | .accepts('req res') 84 | .promises(null) 85 | 86 | .getAuthUri( function (req, res) { 87 | 88 | // Automatic hostname detection + assignment 89 | if (!this._myHostname || this._alwaysDetectHostname) { 90 | this.myHostname(extractHostname(req)); 91 | } 92 | 93 | var params = { 94 | client_id: this.appId() 95 | , redirect_uri: this.myHostname() + this.callbackPath() 96 | } 97 | , authPath = this.authPath() 98 | , url = (/^http/.test(authPath)) 99 | ? authPath 100 | : (this.oauthHost() + authPath) 101 | , additionalParams = this.moreAuthQueryParams 102 | , param; 103 | 104 | if (additionalParams) for (var k in additionalParams) { 105 | param = additionalParams[k]; 106 | if ('function' === typeof param) { 107 | // e.g., for facebook module, param could be 108 | // function () { 109 | // return this._scope && this.scope(); 110 | // } 111 | additionalParams[k] = // cache the function call 112 | param = param.call(this); 113 | } 114 | if ('function' === typeof param) { 115 | // this.scope() itself could be a function 116 | // to allow for dynamic scope determination - e.g., 117 | // function (req, res) { 118 | // return req.session.onboardingPhase; // => "email" 119 | // } 120 | param = param.call(this, req, res); 121 | } 122 | params[k] = param; 123 | } 124 | return url + '?' + querystring.stringify(params); 125 | }) 126 | .requestAuthUri( function (res, authUri) { 127 | res.writeHead(303, {'Location': authUri}); 128 | res.end(); 129 | }) 130 | .getCode( function (req, res) { 131 | var parsedUrl = url.parse(req.url, true); 132 | if (this._authCallbackDidErr(req)) { 133 | return this.breakTo('authCallbackErrorSteps', req, res); 134 | } 135 | return parsedUrl.query && parsedUrl.query.code; 136 | }) 137 | .getAccessToken( function (code) { 138 | var p = this.Promise() 139 | , params = { 140 | client_id: this.appId() 141 | , redirect_uri: this.myHostname() + this.callbackPath() 142 | , code: code 143 | , client_secret: this.appSecret() 144 | } 145 | , url = this.oauthHost() + this.accessTokenPath() 146 | , additionalParams = this.moreAccessTokenParams; 147 | 148 | if (this.accessTokenPath().indexOf("://") != -1) { 149 | // Just in case the access token url uses a different subdomain 150 | // than than the other urls involved in the oauth2 process. 151 | // * cough * ... gowalla 152 | url = this.accessTokenPath(); 153 | } 154 | 155 | if (additionalParams) for (var k in additionalParams) { 156 | params[k] = additionalParams[k]; 157 | } 158 | 159 | var opts = {}; 160 | opts[this.postAccessTokenParamsVia()] = params; 161 | rest[this.accessTokenHttpMethod()](url, opts) 162 | .on('success', function (data, res) { 163 | if ('string' === typeof data) { 164 | data = querystring.parse(data); 165 | } 166 | var aToken = data.access_token; 167 | delete data.access_token; 168 | p.fulfill(aToken, data); 169 | }).on('error', function (data, res) { 170 | p.fail(data); 171 | }); 172 | return p; 173 | }) 174 | .compile( function (accessToken, extra, oauthUser, user) { 175 | var compiled = { 176 | accessToken: accessToken 177 | , oauthUser: oauthUser 178 | , user: user 179 | }; 180 | // extra is any extra params returned by the 181 | // oauth provider in response to the access token 182 | // POST request 183 | for (var k in extra) { 184 | compiled[k] = extra[k]; 185 | } 186 | return compiled; 187 | }) 188 | .getSession( function (req) { 189 | return req.session; 190 | }) 191 | .addToSession( function (sess, auth) { 192 | var _auth = sess.auth || (sess.auth = {}) 193 | , mod = _auth[this.name] || (_auth[this.name] = {}); 194 | _auth.loggedIn = true; 195 | _auth.userId || (_auth.userId = auth.user.id); 196 | mod.user = auth.oauthUser; 197 | mod.accessToken = auth.accessToken; 198 | // this._super() ? 199 | }) 200 | .sendResponse( function (res) { 201 | var redirectTo = this.redirectPath(); 202 | if (!redirectTo) 203 | throw new Error('You must configure a redirectPath'); 204 | res.writeHead(303, {'Location': redirectTo}); 205 | res.end(); 206 | }) 207 | 208 | .authCallbackDidErr( function (req, res) { 209 | return false; 210 | }); 211 | 212 | oauth2.moreAuthQueryParams = {}; 213 | oauth2.moreAccessTokenParams = {}; 214 | oauth2.cloneOnSubmodule.push('moreAuthQueryParams', 'moreAccessTokenParams'); 215 | 216 | oauth2 217 | .authPath('/oauth/authorize') 218 | .accessTokenPath('/oauth/access_token') 219 | .accessTokenHttpMethod('post') 220 | .postAccessTokenParamsVia('query') 221 | .handleAuthCallbackError( function (req, res) { 222 | // TODO Make a better fallback 223 | throw new Error("You must configure handleAuthCallbackError in this module"); 224 | }) 225 | 226 | // Add or over-write existing query params that 227 | // get tacked onto the oauth authorize url. 228 | oauth2.authQueryParam = function (key, val) { 229 | if (arguments.length === 1 && key.constructor == Object) { 230 | for (var k in key) { 231 | this.authQueryParam(k, key[k]); 232 | } 233 | return this; 234 | } 235 | if (val) 236 | this.moreAuthQueryParams[key] = val; 237 | return this; 238 | }; 239 | 240 | // Add or over-write existing params that 241 | // get sent with the oauth access token request. 242 | oauth2.accessTokenParam = function (key, val) { 243 | if (arguments.length === 1 && key.constructor == Object) { 244 | for (var k in key) { 245 | this.accessTokenParam(k, key[k]); 246 | } 247 | return this; 248 | } 249 | if ('function' === typeof val) 250 | val = val(); 251 | if (val) 252 | this.moreAccessTokenParams[key] = val; 253 | return this; 254 | }; 255 | 256 | // removeConfigurable 257 | // removeStep 258 | // undefinedSteps -> [] 259 | // How about module specific and more generic addToSession? Perhaps just do addToSession with null 260 | -------------------------------------------------------------------------------- /lib/modules/oauth.js: -------------------------------------------------------------------------------- 1 | var everyModule = require('./everymodule') 2 | , OAuth = require('oauth').OAuth 3 | , url = require('url') 4 | , extractHostname = require('../utils').extractHostname; 5 | 6 | var oauth = module.exports = 7 | everyModule.submodule('oauth') 8 | .configurable({ 9 | apiHost: 'e.g., https://api.twitter.com' 10 | , oauthHost: 'the host for the OAuth provider' 11 | , requestTokenPath: "the path on the OAuth provider's domain where we request the request token, e.g., /oauth/request_token" 12 | , accessTokenPath: "the path on the OAuth provider's domain where we request the access token, e.g., /oauth/access_token" 13 | , authorizePath: 'the path on the OAuth provider where you direct a visitor to login, e.g., /oauth/authorize' 14 | , sendCallbackWithAuthorize: 'whether you want oauth_callback=... as a query param send with your request to /oauth/authorize' 15 | , consumerKey: 'the api key provided by the OAuth provider' 16 | , consumerSecret: 'the api secret provided by the OAuth provider' 17 | , myHostname: 'e.g., http://localhost:3000 . Notice no trailing slash' 18 | , alwaysDetectHostname: 'does not cache myHostname once. Instead, re-detect it on every request. Good for multiple subdomain architectures' 19 | , redirectPath: 'Where to redirect to after a failed or successful OAuth authorization' 20 | , convertErr: 'a function (data) that extracts an error message from data arg, where `data` is what is returned from a failed OAuth request' 21 | , authCallbackDidErr: 'Define the condition for the auth module determining if the auth callback url denotes a failure. Returns true/false.' 22 | }) 23 | .definit( function () { 24 | this.oauth = new OAuth( 25 | this.oauthHost() + this.requestTokenPath() 26 | , this.oauthHost() + this.accessTokenPath() 27 | , this.consumerKey() 28 | , this.consumerSecret() 29 | , '1.0', null, 'HMAC-SHA1'); 30 | }) 31 | 32 | .get('entryPath', 33 | 'the link a user follows, whereupon you redirect them to the 3rd party OAuth provider dialog - e.g., "/auth/twitter"') 34 | .step('getRequestToken') 35 | .description('asks OAuth Provider for a request token') 36 | .accepts('req res') 37 | .promises('token tokenSecret') 38 | .step('storeRequestToken') 39 | .description('stores the request token and secret in the session') 40 | .accepts('req token tokenSecret') 41 | .promises(null) 42 | .step('redirectToProviderAuth') 43 | .description('sends the user to authorization on the OAuth provider site') 44 | .accepts('res token') 45 | .promises(null) 46 | 47 | .get('callbackPath', 48 | 'the callback path that the 3rd party OAuth provider redirects to after an OAuth authorization result - e.g., "/auth/twitter/callback"') 49 | .step('extractTokenAndVerifier') 50 | .description('extracts the request token and verifier from the url query') 51 | .accepts('req res') 52 | .promises('requestToken verifier') 53 | .canBreakTo('handleDuplicateCallbackRequest') 54 | .canBreakTo('authCallbackErrorSteps') 55 | .step('getSession') 56 | .accepts('req') 57 | .promises('session') 58 | .step('rememberTokenSecret') 59 | .description('retrieves the request token secret from the session') 60 | .accepts('session') 61 | .promises('requestTokenSecret') 62 | .step('getAccessToken') 63 | .description('requests an access token from the OAuth provider') 64 | .accepts('requestToken requestTokenSecret verifier') 65 | .promises('accessToken accessTokenSecret params') 66 | .step('fetchOAuthUser') 67 | .accepts('accessToken accessTokenSecret params') 68 | .promises('oauthUser') 69 | .step('findOrCreateUser') 70 | .accepts('session accessToken accessTokenSecret oauthUser') 71 | .promises('user') 72 | .step('compileAuth') 73 | .accepts('accessToken accessTokenSecret oauthUser user') 74 | .promises('auth') 75 | .step('addToSession') 76 | .accepts('session auth') 77 | .promises(null) 78 | .step('sendResponse') 79 | .accepts('res') 80 | .promises(null) 81 | 82 | .stepseq('handleDuplicateCallbackRequest', 83 | 'handles the case if you manually click the callback link on Twitter, but Twitter has already sent a redirect request to the callback path with the same token') 84 | .step('waitForPriorRequestToWriteSession') 85 | .accepts('req res') 86 | .promises(null) 87 | .step('sendResponse') 88 | 89 | .stepseq('authCallbackErrorSteps') 90 | .step('handleAuthCallbackError', 91 | 'a request handler that intercepts a failed authorization message sent from the OAuth provider to your service. e.g., the request handler for "/auth/twitter/callback?denied=blahblahblahblahblah"') 92 | .accepts('req res') 93 | .promises(null) 94 | 95 | .getRequestToken( function (req, res) { 96 | 97 | // Automatic hostname detection + assignment 98 | if (!this._myHostname || this._alwaysDetectHostname) { 99 | this.myHostname(extractHostname(req)); 100 | } 101 | 102 | var p = this.Promise(); 103 | this.oauth.getOAuthRequestToken({ oauth_callback: this._myHostname + this._callbackPath }, function (err, token, tokenSecret, params) { 104 | if (err && !~(err.data.indexOf('Invalid / expired Token'))) { 105 | return p.fail(err); 106 | } 107 | p.fulfill(token, tokenSecret); 108 | }); 109 | return p; 110 | }) 111 | .storeRequestToken( function (req, token, tokenSecret) { 112 | var sess = req.session 113 | , _auth = sess.auth || (sess.auth = {}) 114 | , _provider = _auth[this.name] || (_auth[this.name] = {}); 115 | _provider.token = token; 116 | _provider.tokenSecret = tokenSecret; 117 | }) 118 | .redirectToProviderAuth( function (res, token) { 119 | // Note: Not all oauth modules need oauth_callback as a uri query parameter. As far as I know, only readability's 120 | // module needs it as a uri query parameter. However, in cases such as twitter, it allows you to over-ride 121 | // the callback url settings at dev.twitter.com from one place, your app code, rather than in two places -- i.e., 122 | // your app code + dev.twitter.com app settings. 123 | var redirectTo = this._oauthHost + this._authorizePath + '?oauth_token=' + token; 124 | if (this._sendCallbackWithAuthorize) { 125 | redirectTo += '&oauth_callback=' + this._myHostname + this._callbackPath; 126 | } 127 | res.writeHead(303, { 'Location': redirectTo }); 128 | res.end(); 129 | }) 130 | 131 | // Steps for GET `callbackPath` 132 | .extractTokenAndVerifier( function (req, res) { 133 | if (this._authCallbackDidErr && this._authCallbackDidErr(req)) { 134 | return this.breakTo('authCallbackErrorSteps', req, res); 135 | } 136 | var parsedUrl = url.parse(req.url, true) 137 | , query = parsedUrl.query 138 | , requestToken = query && query.oauth_token 139 | , verifier = query && query.oauth_verifier 140 | 141 | , sess = req.session 142 | , promise 143 | , _auth = sess.auth || (sess.auth = {}) 144 | , name = this.name 145 | , mod = _auth[name] || (_auth[name] = {}); 146 | if ((name === 'twitter') && (mod.token === requestToken) && (mod.verifier === verifier)) { 147 | return this.breakTo('handleDuplicateCallbackRequest', req, res); 148 | } 149 | 150 | promise = this.Promise(); 151 | mod.verifier = verifier; 152 | sess.save( function (err) { 153 | if (err) return promise.fail(err); 154 | promise.fulfill(requestToken, verifier); 155 | }); 156 | return promise; 157 | }) 158 | .getSession( function(req) { 159 | return req.session; 160 | }) 161 | .rememberTokenSecret( function (sess) { 162 | return sess.auth && sess.auth[this.name] && sess.auth[this.name].tokenSecret; 163 | }) 164 | .getAccessToken( function (reqToken, reqTokenSecret, verifier) { 165 | var promise = this.Promise(); 166 | this.oauth.getOAuthAccessToken(reqToken, reqTokenSecret, verifier, function (err, accessToken, accessTokenSecret, params) { 167 | if (err && !~(err.data.indexOf('Invalid / expired Token'))) { 168 | return promise.fail(err); 169 | } 170 | promise.fulfill(accessToken, accessTokenSecret, params); 171 | }); 172 | return promise; 173 | }) 174 | .compileAuth( function (accessToken, accessTokenSecret, oauthUser, user) { 175 | return { 176 | accessToken: accessToken 177 | , accessTokenSecret: accessTokenSecret 178 | , oauthUser: oauthUser 179 | , user: user 180 | }; 181 | }) 182 | .addToSession( function (sess, auth) { 183 | var promise = this.Promise() 184 | , _auth = sess.auth 185 | , mod = _auth[this.name]; 186 | _auth.loggedIn = true; 187 | _auth.userId || (_auth.userId = auth.user.id); 188 | mod.user = auth.oauthUser; 189 | mod.accessToken = auth.accessToken; 190 | mod.accessTokenSecret = auth.accessTokenSecret; 191 | // this._super() ? 192 | sess.save( function (err) { 193 | if (err) return promise.fail(err); 194 | promise.fulfill(); 195 | }); 196 | return promise; 197 | }) 198 | .sendResponse( function (res, data) { 199 | var redirectTo = this.redirectPath(); 200 | if (!redirectTo) 201 | throw new Error('You must configure a redirectPath'); 202 | res.writeHead(303, {'Location': redirectTo}); 203 | res.end(); 204 | }) 205 | 206 | .waitForPriorRequestToWriteSession( function (req, res) { 207 | var promise = this.Promise(); 208 | function check (self, sess, res, promise) { 209 | if (sess.auth[self.name].accessToken) { 210 | return promise.fulfill(); 211 | } 212 | 213 | setTimeout(function () { 214 | sess.reload( function (err) { 215 | if (err) return promise.fail(err); 216 | check(self, req.session, res, promise); 217 | }); 218 | }, 100); 219 | } 220 | check(this, req.session, res, promise); 221 | return promise; 222 | }); 223 | 224 | // Defaults inherited by submodules 225 | oauth 226 | .requestTokenPath('/oauth/request_token') 227 | .authorizePath('/oauth/authorize') 228 | .accessTokenPath('/oauth/access_token') 229 | .handleAuthCallbackError( function (req, res) { 230 | // TODO Make a better fallback 231 | throw new Error("You must configure handleAuthCallbackError in this module"); 232 | }) 233 | .sendCallbackWithAuthorize(true); 234 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , everyauth = require('../index') 3 | , conf = require('./conf'); 4 | 5 | everyauth.debug = true; 6 | 7 | var usersById = {}; 8 | var nextUserId = 0; 9 | 10 | function addUser (source, sourceUser) { 11 | var user; 12 | if (arguments.length === 1) { // password-based 13 | user = sourceUser = source; 14 | user.id = ++nextUserId; 15 | return usersById[nextUserId] = user; 16 | } else { // non-password-based 17 | user = usersById[++nextUserId] = {id: nextUserId}; 18 | user[source] = sourceUser; 19 | } 20 | return user; 21 | } 22 | 23 | var usersByVimeoId = {}; 24 | var usersByJustintvId = {}; 25 | var usersBy37signalsId = {}; 26 | var usersByTumblrName = {}; 27 | var usersByDropboxId = {}; 28 | var usersByFbId = {}; 29 | var usersByTwitId = {}; 30 | var usersByGhId = {}; 31 | var usersByInstagramId = {}; 32 | var usersByFoursquareId = {}; 33 | var usersByGowallaId = {}; 34 | var usersByLinkedinId = {}; 35 | var usersByGoogleId = {}; 36 | var usersByYahooId = {}; 37 | var usersByGoogleHybridId = {}; 38 | var usersByReadabilityId = {}; 39 | var usersByBoxId = {}; 40 | var usersByOpenId = {}; 41 | var usersByLogin = { 42 | 'brian@example.com': addUser({ login: 'brian@example.com', password: 'password'}) 43 | }; 44 | 45 | everyauth.everymodule 46 | .findUserById( function (id, callback) { 47 | callback(null, usersById[id]); 48 | }); 49 | 50 | everyauth 51 | .openid 52 | .myHostname('http://local.host:3000') 53 | .findOrCreateUser( function (session, userMetadata) { 54 | return usersByOpenId[userMetadata.claimedIdentifier] || 55 | (usersByOpenId[userMetadata.claimedIdentifier] = addUser('openid', userMetadata)); 56 | }) 57 | .redirectPath('/'); 58 | 59 | 60 | everyauth 61 | .facebook 62 | .appId(conf.fb.appId) 63 | .appSecret(conf.fb.appSecret) 64 | .findOrCreateUser( function (session, accessToken, accessTokenExtra, fbUserMetadata) { 65 | return usersByFbId[fbUserMetadata.id] || 66 | (usersByFbId[fbUserMetadata.id] = addUser('facebook', fbUserMetadata)); 67 | }) 68 | .redirectPath('/'); 69 | 70 | everyauth 71 | .twitter 72 | .consumerKey(conf.twit.consumerKey) 73 | .consumerSecret(conf.twit.consumerSecret) 74 | .findOrCreateUser( function (sess, accessToken, accessSecret, twitUser) { 75 | return usersByTwitId[twitUser.id] || (usersByTwitId[twitUser.id] = addUser('twitter', twitUser)); 76 | }) 77 | .redirectPath('/'); 78 | 79 | everyauth 80 | .password 81 | .loginWith('email') 82 | .getLoginPath('/login') 83 | .postLoginPath('/login') 84 | .loginView('login.jade') 85 | // .loginLocals({ 86 | // title: 'Login' 87 | // }) 88 | // .loginLocals(function (req, res) { 89 | // return { 90 | // title: 'Login' 91 | // } 92 | // }) 93 | .loginLocals( function (req, res, done) { 94 | setTimeout( function () { 95 | done(null, { 96 | title: 'Async login' 97 | }); 98 | }, 200); 99 | }) 100 | .authenticate( function (login, password) { 101 | var errors = []; 102 | if (!login) errors.push('Missing login'); 103 | if (!password) errors.push('Missing password'); 104 | if (errors.length) return errors; 105 | var user = usersByLogin[login]; 106 | if (!user) return ['Login failed']; 107 | if (user.password !== password) return ['Login failed']; 108 | return user; 109 | }) 110 | 111 | .getRegisterPath('/register') 112 | .postRegisterPath('/register') 113 | .registerView('register.jade') 114 | // .registerLocals({ 115 | // title: 'Register' 116 | // }) 117 | // .registerLocals(function (req, res) { 118 | // return { 119 | // title: 'Sync Register' 120 | // } 121 | // }) 122 | .registerLocals( function (req, res, done) { 123 | setTimeout( function () { 124 | done(null, { 125 | title: 'Async Register' 126 | }); 127 | }, 200); 128 | }) 129 | .validateRegistration( function (newUserAttrs, errors) { 130 | var login = newUserAttrs.login; 131 | if (usersByLogin[login]) errors.push('Login already taken'); 132 | return errors; 133 | }) 134 | .registerUser( function (newUserAttrs) { 135 | var login = newUserAttrs[this.loginKey()]; 136 | return usersByLogin[login] = addUser(newUserAttrs); 137 | }) 138 | 139 | .loginSuccessRedirect('/') 140 | .registerSuccessRedirect('/'); 141 | 142 | everyauth.github 143 | .appId(conf.github.appId) 144 | .appSecret(conf.github.appSecret) 145 | .findOrCreateUser( function (sess, accessToken, accessTokenExtra, ghUser) { 146 | return usersByGhId[ghUser.id] || (usersByGhId[ghUser.id] = addUser('github', ghUser)); 147 | }) 148 | .redirectPath('/'); 149 | 150 | everyauth.instagram 151 | .appId(conf.instagram.clientId) 152 | .appSecret(conf.instagram.clientSecret) 153 | .scope('basic') 154 | .findOrCreateUser( function (sess, accessToken, accessTokenExtra, hipster) { 155 | return usersByInstagramId[hipster.id] || (usersByInstagramId[hipster.id] = addUser('instagram', hipster)); 156 | }) 157 | .redirectPath('/'); 158 | 159 | everyauth.foursquare 160 | .appId(conf.foursquare.clientId) 161 | .appSecret(conf.foursquare.clientSecret) 162 | .findOrCreateUser( function (sess, accessTok, accessTokExtra, addict) { 163 | return usersByFoursquareId[addict.id] || (usersByFoursquareId[addict.id] = addUser('foursquare', addict)); 164 | }) 165 | .redirectPath('/'); 166 | 167 | everyauth.gowalla 168 | .appId(conf.gowalla.apiKey) 169 | .appSecret(conf.gowalla.apiSecret) 170 | .moduleErrback( function(err) { 171 | console.log("moduleErrback for Gowalla", err); 172 | }) 173 | .findOrCreateUser( function (sess, accessToken, accessTokenExtra, loser) { 174 | return usersByGowallaId[loser.url] || (usersByGowallaId[loser.url] = addUser('gowalla', loser)); 175 | }) 176 | .redirectPath('/'); 177 | 178 | everyauth.linkedin 179 | .consumerKey(conf.linkedin.apiKey) 180 | .consumerSecret(conf.linkedin.apiSecret) 181 | .findOrCreateUser( function (sess, accessToken, accessSecret, linkedinUser) { 182 | return usersByLinkedinId[linkedinUser.id] || (usersByLinkedinId[linkedinUser.id] = addUser('linkedin', linkedinUser)); 183 | }) 184 | .redirectPath('/'); 185 | 186 | everyauth.google 187 | .appId(conf.google.clientId) 188 | .appSecret(conf.google.clientSecret) 189 | .scope('https://www.google.com/m8/feeds/') 190 | .findOrCreateUser( function (sess, accessToken, extra, googleUser) { 191 | googleUser.refreshToken = extra.refresh_token; 192 | googleUser.expiresIn = extra.expires_in; 193 | return usersByGoogleId[googleUser.id] || (usersByGoogleId[googleUser.id] = addUser('google', googleUser)); 194 | }) 195 | .redirectPath('/'); 196 | 197 | everyauth.yahoo 198 | .consumerKey(conf.yahoo.consumerKey) 199 | .consumerSecret(conf.yahoo.consumerSecret) 200 | .findOrCreateUser( function (sess, accessToken, accessSecret, yahooUser) { 201 | return usersByYahooId[yahooUser.id] || (usersByYahooId[yahooUser.id] = addUser('yahoo', yahooUser)); 202 | }) 203 | .redirectPath('/'); 204 | 205 | everyauth.googlehybrid 206 | .consumerKey(conf.google.clientId) 207 | .consumerSecret(conf.google.clientSecret) 208 | .scope(['http://docs.google.com/feeds/','http://spreadsheets.google.com/feeds/']) 209 | .findOrCreateUser( function(session, userAttributes) { 210 | return usersByGoogleHybridId[userAttributes.claimedIdentifier] || (usersByGoogleHybridId[userAttributes.claimedIdentifier] = addUser('googlehybrid', userAttributes)); 211 | }) 212 | .redirectPath('/') 213 | 214 | everyauth.readability 215 | .consumerKey(conf.readability.consumerKey) 216 | .consumerSecret(conf.readability.consumerSecret) 217 | .findOrCreateUser( function (sess, accessToken, accessSecret, reader) { 218 | return usersByReadabilityId[reader.username] || (usersByReadabilityId[reader.username] = addUser('readability', reader)); 219 | }) 220 | .redirectPath('/'); 221 | 222 | everyauth 223 | .dropbox 224 | .consumerKey(conf.dropbox.consumerKey) 225 | .consumerSecret(conf.dropbox.consumerSecret) 226 | .findOrCreateUser( function (sess, accessToken, accessSecret, dropboxUserMetadata) { 227 | return usersByDropboxId[dropboxUserMetadata.uid] || 228 | (usersByDropboxId[dropboxUserMetadata.uid] = addUser('dropbox', dropboxUserMetadata)); 229 | }) 230 | .redirectPath('/') 231 | 232 | everyauth.vimeo 233 | .consumerKey(conf.vimeo.consumerKey) 234 | .consumerSecret(conf.vimeo.consumerSecret) 235 | .findOrCreateUser( function (sess, accessToken, accessSecret, vimeoUser) { 236 | return usersByVimeoId[vimeoUser.id] || 237 | (usersByVimeoId[vimeoUser.id] = vimeoUser); 238 | }) 239 | .redirectPath('/') 240 | 241 | everyauth.justintv 242 | .consumerKey(conf.justintv.consumerKey) 243 | .consumerSecret(conf.justintv.consumerSecret) 244 | .findOrCreateUser( function (sess, accessToken, accessSecret, justintvUser) { 245 | return usersByJustintvId[justintvUser.id] || 246 | (usersByJustintvId[justintvUser.id] = addUser('justintv', justintvUser)); 247 | }) 248 | .redirectPath('/') 249 | 250 | everyauth['37signals'] 251 | .appId(conf['37signals'].clientId) 252 | .appSecret(conf['37signals'].clientSecret) 253 | .findOrCreateUser( function (sess, accessToken, accessSecret, _37signalsUser) { 254 | return usersBy37signalsId[_37signalsUser.id] || 255 | (usersBy37signalsId[_37signalsUser.identity.id] = addUser('37signals', _37signalsUser)); 256 | }) 257 | .redirectPath('/') 258 | 259 | everyauth.tumblr 260 | .consumerKey(conf.tumblr.consumerKey) 261 | .consumerSecret(conf.tumblr.consumerSecret) 262 | .findOrCreateUser( function (sess, accessToken, accessSecret, tumblrUser) { 263 | return usersByTumblrName[tumblrUser.name] || 264 | (usersByTumblrName[tumblrUser.name] = addUser('tumblr', tumblrUser)); 265 | }) 266 | .redirectPath('/'); 267 | 268 | everyauth.box 269 | .apiKey(conf.box.apiKey) 270 | .findOrCreateUser( function (sess, authToken, boxUser) { 271 | return usersByBoxId[boxUser.user_id] || 272 | (usersByDropboxId[boxUser.user_id] = addUser('box', boxUser)); 273 | }) 274 | .redirectPath('/'); 275 | 276 | var app = express.createServer( 277 | express.bodyParser() 278 | , express.static(__dirname + "/public") 279 | , express.cookieParser() 280 | , express.session({ secret: 'htuayreve'}) 281 | , everyauth.middleware() 282 | ); 283 | 284 | app.configure( function () { 285 | app.set('view engine', 'jade'); 286 | }); 287 | 288 | app.get('/', function (req, res) { 289 | res.render('home'); 290 | }); 291 | 292 | everyauth.helpExpress(app); 293 | 294 | app.listen(3000); 295 | 296 | console.log('Go to http://local.host:3000'); 297 | -------------------------------------------------------------------------------- /lib/modules/everymodule.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | , Step = require('../step') 3 | , StepSequence = require('../stepSequence') 4 | , RouteTriggeredSequence = require('../routeTriggeredSequence') 5 | , clone = require('../utils').clone 6 | , Promise = require('../promise'); 7 | 8 | var routeDescPrefix = { 9 | get: 'ROUTE (GET)' 10 | , post: 'ROUTE (POST)' 11 | }; 12 | routeDescPrefix.GET = routeDescPrefix.get; 13 | routeDescPrefix.POST = routeDescPrefix.post; 14 | 15 | function route (method) { 16 | return function (alias, description) { 17 | if (description) 18 | description = routeDescPrefix[method] + ' - ' + description; 19 | this.configurable(alias, description); 20 | var name = method + ':' + alias; 21 | this._currSeq = 22 | this._stepSequences[name] || (this._stepSequences[name] = new RouteTriggeredSequence(name, this)); 23 | return this; 24 | }; 25 | } 26 | 27 | var everyModule = module.exports = { 28 | name: 'everymodule' 29 | , definit: function (fn) { 30 | // Remove any prior `init` that was assigned 31 | // directly to the object via definit and not 32 | // assigned via prototypal inheritance 33 | if (this.hasOwnProperty('init')) 34 | delete this.init; 35 | 36 | var _super = this.init; 37 | // since this.hasOwnProperty('init') is false 38 | 39 | this.init = function () { 40 | this._super = _super; 41 | fn.apply(this, arguments); 42 | delete this._super; 43 | 44 | // Do module compilation here, too 45 | }; 46 | return this; 47 | } 48 | , get: route('get') 49 | , post: route('post') 50 | , stepseq: function (name, description) { 51 | this.configurable(name, description); 52 | this._currSeq = 53 | this._stepSequences[name] || (this._stepSequences[name] = new StepSequence(name, this)); 54 | return this; 55 | } 56 | , configurable: function (arg, description, wrapper) { 57 | if (!arguments.length) 58 | return this._configurable; 59 | var property; 60 | if (arg.constructor === Object) { 61 | for (property in arg) { 62 | description = arg[property]; 63 | this.configurable(property, description); 64 | } 65 | } else if (typeof arg === 'string') { 66 | property = arg; 67 | if (property.indexOf(' ') !== -1) { 68 | // e.g., property === 'apiHost appId appSecret' 69 | var self = this; 70 | property.split(/\s+/).forEach( function (_property) { 71 | self.configurable(_property); 72 | }); 73 | return this; 74 | } 75 | 76 | // Else we have a single property name 77 | // (Base Case) 78 | if (!this[property]) 79 | this[property] = function (setTo) { 80 | var k = '_' + property; 81 | if (!arguments.length) { 82 | // TODO this.everyauth is not yet available here in some contexts 83 | // For example, when we set and try to access a scope in an auth module definition 84 | // but if you look in index, everyauth is not assigned to the module until after it is 85 | // required 86 | if (this.everyauth && this.everyauth.debug && 'undefined' === typeof this[k]) { 87 | var debugMsg = 'WARNING: You are trying to access the attribute/method configured by `' + 88 | property + '`, which you did not configure. Time to configure it.'; 89 | console.log(debugMsg); 90 | console.trace(); 91 | } 92 | return this[k]; 93 | } 94 | this[k] = setTo; 95 | return this; 96 | }; 97 | this._configurable[property] = description || 98 | this.configurable[property] || 99 | 'No Description'; 100 | 101 | // Add configurable to submodules that inherit from this 102 | // supermodule 103 | for (var name in this.submodules) { 104 | this.submodules[name].configurable(arg, description); 105 | } 106 | } 107 | return this; 108 | } 109 | 110 | , step: function (name) { 111 | var steps = this._steps 112 | , sequence = this._currSeq; 113 | 114 | if (!sequence) 115 | throw new Error("You can only declare a step after declaring a route alias via `get(...)` or `post(...)`."); 116 | 117 | sequence.orderedStepNames.push(name); 118 | 119 | this._currentStep = 120 | steps[name] || (steps[name] = new Step(name, this)); 121 | 122 | // For configuring what the actual business 123 | // logic is: 124 | // fb.step('fetchOAuthUser') generates the method 125 | // fb.fetchOAuthUser whose logic can be configured like 126 | // fb.fetchOAuthUser( function (...) { 127 | // // Business logic goes here 128 | // } ); 129 | this.configurable(name, 130 | 'STEP FN [' + name + '] function encapsulating the logic for the step `' + name + '`.'); 131 | return this; 132 | } 133 | 134 | , accepts: function (input) { 135 | var step = this._currentStep; 136 | step.accepts = input 137 | ? input.split(/\s+/) 138 | : null; 139 | return this; 140 | } 141 | 142 | , promises: function (output) { 143 | var step = this._currentStep; 144 | step.promises = output 145 | ? output.split(/\s+/) 146 | : null; 147 | return this; 148 | } 149 | 150 | , description: function (desc) { 151 | var step = this._currentStep; 152 | step.description = desc; 153 | 154 | if (desc) 155 | desc = 'STEP FN [' + step.name + '] - ' + desc; 156 | this.configurable(step.name, desc); 157 | return this; 158 | } 159 | 160 | , stepTimeout: function (millis) { 161 | var step = this._currentStep; 162 | step.timeout = millis; 163 | return this; 164 | } 165 | 166 | , stepErrback: function (fn) { 167 | var step = this._currentStep; 168 | step.errback = fn; 169 | return this; 170 | } 171 | 172 | , canBreakTo: function (sequenceName) { 173 | // TODO Implement this (like static typing) 174 | // unless `canBreakTo` only needed for 175 | // readability 176 | return this; 177 | } 178 | 179 | 180 | , cloneOnSubmodule: ['cloneOnSubmodule', '_configurable'] 181 | 182 | , submodules: {} 183 | 184 | /** 185 | * Creates a new submodule using prototypal 186 | * inheritance 187 | */ 188 | , submodule: function (name) { 189 | var submodule = Object.create(this) 190 | , self = this; 191 | 192 | // So that when we add configurables after 193 | // to the supermodule after the submodule 194 | // creation, we can propagate those configurables 195 | // to the supermodule's submodules 196 | this.submodules[name] = submodule; 197 | submodule.submodules = {}; 198 | 199 | this.cloneOnSubmodule.forEach( 200 | function (toClone) { 201 | submodule[toClone] = clone(self[toClone]); 202 | } 203 | ); 204 | 205 | var seqs = this._stepSequences 206 | , newSeqs = submodule._stepSequences = {}; 207 | for (var seqName in seqs) { 208 | newSeqs[seqName] = seqs[seqName].clone(submodule); 209 | } 210 | 211 | var steps = this._steps 212 | , newSteps = submodule._steps = {}; 213 | for (var stepName in steps) { 214 | newSteps[stepName] = steps[stepName].clone(stepName, submodule); 215 | } 216 | 217 | submodule.name = name; 218 | return submodule; 219 | } 220 | 221 | , validateSteps: function () { 222 | for (var seqName in this._stepSequences) { 223 | this._stepSequences[seqName].checkSteps(); 224 | } 225 | } 226 | 227 | /** 228 | * Decorates the app with the routes required of the 229 | * module 230 | */ 231 | , routeApp: function (app) { 232 | if (this.init) this.init(); 233 | var self = this 234 | , routes = this._routes 235 | , methods = ['get', 'post']; 236 | for (var method in routes) { 237 | for (var routeAlias in routes[method]) { 238 | var path = self[routeAlias](); 239 | if (!path) 240 | throw new Error('You have not defined a path for the route alias ' + routeAlias + '.'); 241 | var seq = routes[method][routeAlias]; 242 | 243 | // This kicks off a sequence of steps based on a 244 | // route 245 | // Creates a new chain of promises and exposes the leading promise 246 | // to the incoming (req, res) pair from the route handler 247 | app[method](path, seq.routeHandler.bind(seq)); 248 | } 249 | } 250 | } 251 | 252 | , Promise: function (values) { 253 | return new Promise(this, values); 254 | } 255 | 256 | /** 257 | * breakTo(sequenceName, arg1, arg2, ...); 258 | * [arg1, arg2, ...] are the arguments passed to 259 | * the `sequence.start(...)` where sequence is the 260 | * sequence with name `sequenceName` 261 | * @param {String} sequenceName 262 | */ 263 | , breakTo: function (sequenceName) { 264 | // TODO Garbage collect the abandoned sequence 265 | var seq = this._stepSequences[sequenceName] 266 | , args = Array.prototype.slice.call(arguments, 1); 267 | if (!seq) { 268 | throw new Error('You are trying to break to a sequence named `' + sequenceName + '`, but there is no sequence with that name in the auth module, `' + this.name + '`.'); 269 | } 270 | seq = seq.materialize(); 271 | seq.initialArgs = args; 272 | throw seq; 273 | } 274 | 275 | // _steps maps step names to step objects 276 | // A step object is { accepts: [...], promises: [...] } 277 | , _steps: {} 278 | 279 | , _stepSequences: {} 280 | 281 | // _configurable maps parameter names to descriptions 282 | // It is used for introspection with this.configurable() 283 | , _configurable: {} 284 | }; 285 | 286 | Object.defineProperty(everyModule, 'shouldSetup', { get: function () { 287 | return ! Object.keys(this.submodules).length; 288 | }}); 289 | 290 | Object.defineProperty(everyModule, '_routes', { get: function () { 291 | var seqs = this._stepSequences 292 | , methods = ['get', 'post']; 293 | return Object.keys(seqs).filter( function (seqName) { 294 | return ~methods.indexOf(seqName.split(':')[0]); 295 | }).reduce( function (_routes, routeName) { 296 | var parts = routeName.split(':') 297 | , method = parts[0] 298 | , routeAlias = parts[1]; 299 | _routes[method] || (_routes[method] = {}); 300 | _routes[method][routeAlias] = seqs[routeName]; 301 | return _routes; 302 | }, {}); 303 | }}); 304 | 305 | Object.defineProperty(everyModule, 'route', { 306 | get: function () { 307 | return this._routes; 308 | } 309 | }); 310 | 311 | Object.defineProperty(everyModule, 'routes', {get: function () { 312 | var arr = [] 313 | , _routes = this._routes 314 | , _descriptions = this._configurable 315 | , aliases 316 | , self = this; 317 | for (var method in _routes) { 318 | for (var alias in _routes[method]) { 319 | method = method.toUpperCase(); 320 | arr.push(method + ' (' + alias + ') [' + 321 | self[alias]() + ']' + 322 | _descriptions[alias].replace(routeDescPrefix[method], '')); 323 | } 324 | } 325 | return arr; 326 | }}); 327 | 328 | everyModule 329 | .configurable({ 330 | moduleTimeout: 'how long to wait per step ' + 331 | 'before timing out and invoking any ' + 332 | 'timeout callbacks' 333 | , moduleErrback: 'THE error callback that is invoked' + 334 | 'any time an error occurs in the module; defaults to `throw` wrapper' 335 | , logoutRedirectPath: 'where to redirect the app upon logging out' 336 | , findUserById: 'function for fetching a user by his/her id -- used to assign to `req.user` - function (userId, callback) where function callback (err, user)' 337 | }) 338 | .get('logoutPath') 339 | .step('handleLogout') 340 | .accepts('req res') 341 | .promises(null) 342 | .logoutPath('/logout') 343 | .handleLogout( function (req, res) { 344 | req.logout(); 345 | res.writeHead(303, { 'Location': this.logoutRedirectPath() }); 346 | res.end(); 347 | }) 348 | .logoutRedirectPath('/'); 349 | 350 | everyModule.moduleTimeout(10000); 351 | everyModule.moduleErrback( function (err) { 352 | throw err; 353 | }); 354 | -------------------------------------------------------------------------------- /lib/modules/password.js: -------------------------------------------------------------------------------- 1 | var everyModule = require('./everymodule'); 2 | 3 | /** 4 | * This is used to generate the render methods used with 5 | * 6 | * - `displayLogin` 7 | * - `respondToLoginFail` 8 | * - `displayRegister` 9 | * - `respondToRegistrationFail` 10 | * 11 | * The generated functions have the signature 12 | * 13 | * function (req, res, *extraParams) {...} 14 | * 15 | * @param {String} type is either 'login' or 'register' 16 | * @param {Function} localsDefault is a function that returns the default 17 | * locals object. It has the signature 18 | * 19 | * function (*extraParams) {...} 20 | * 21 | * So, for example, if the generated function can handle incoming args: 22 | * 23 | * function render (req, res, errors, data) 24 | * 25 | * , then the `localsDefault` signature can see the params 26 | * 27 | * function localsDefault (errors, data) 28 | * 29 | * @returns {Function} a generated render function with signature 30 | * 31 | * function (req, res, *extraParams) {...} 32 | */ 33 | function renderGenerator (type, localsDefault) { 34 | return function render (req, res) { 35 | var locals, render, layout 36 | , extraLocals 37 | , view = this[type + 'View'](), arity 38 | , trailingArgs = Array.prototype.slice.call(arguments, 2); 39 | 40 | if (res.render) { 41 | locals = 42 | localsDefault.apply(this, trailingArgs); 43 | layout = this['_' + type + 'Layout']; 44 | render = function render (locals) { 45 | if ('undefined' !== typeof layout) { 46 | locals.layout = layout; 47 | } 48 | res.render(view, locals); 49 | }; 50 | 51 | extraLocals = this['_' + type + 'Locals']; 52 | if ('function' !== typeof extraLocals) { 53 | for (var k in extraLocals) { 54 | locals[k] = extraLocals[k]; 55 | } 56 | return render(locals); 57 | } 58 | 59 | arity = extraLocals.length; 60 | if (arity === 2) { 61 | // Dynamic sync locals 62 | extraLocals = extraLocals(req, res); 63 | for (var k in extraLocals) { 64 | locals[k] = extraLocals[k]; 65 | } 66 | return render(locals); 67 | } else if (arity === 3) { 68 | // Dynamic async locals 69 | return extraLocals(req, req, function (err, extraLocals) { 70 | if (err) throw err; // TODO Call global configurable error handler 71 | for (var k in extraLocals) { 72 | locals[k] = extraLocals[k]; 73 | } 74 | return render(locals); 75 | }); 76 | } else { 77 | throw new Error('Your `locals` function must have arity 2 or 3'); 78 | } 79 | } else { 80 | res.writeHead(200, {'Content-Type': 'text/html'}); 81 | if ('function' === typeof view) { 82 | res.end(view.apply(this, trailingArgs)); 83 | } else { 84 | res.end(loginView); 85 | } 86 | } 87 | }; 88 | } 89 | 90 | var renderLogin = renderGenerator('login', 91 | function () { 92 | var locals = {}; 93 | locals[this.loginKey()] = null; 94 | return locals; 95 | } 96 | ); 97 | 98 | var renderLoginFail = renderGenerator('login', 99 | function (errors, login) { 100 | var locals = {errors: errors}; 101 | locals[this.loginKey()] = login; 102 | return locals; 103 | } 104 | ); 105 | 106 | var renderRegister = renderGenerator('register', 107 | function () { 108 | var locals = {}; 109 | locals.userParams = {}; 110 | return locals; 111 | } 112 | ); 113 | 114 | var renderRegisterFail = renderGenerator('register', 115 | function (errors, newUserAttributes) { 116 | return { 117 | errors: errors 118 | , userParams: newUserAttributes}; 119 | } 120 | ); 121 | 122 | /** 123 | * Let's define our password module 124 | */ 125 | var password = module.exports = 126 | everyModule.submodule('password') 127 | .configurable({ 128 | loginFormFieldName: 'the name of the login field. Same as what you put in your login form ' 129 | + '- e.g., if , then loginFormFieldName ' 130 | + 'should be set to "username".' 131 | , passwordFormFieldName: 'the name of the login field. Same as what you put in your login form ' 132 | + '- e.g., if , then passwordFormFieldName ' 133 | + 'should be set to "pswd".' 134 | , loginHumanName: 'the human readable name of login -- e.g., "login" or "email"' 135 | , loginKey: 'the name of the login field in your data store -- e.g., "email"; defaults to "login"' 136 | , loginWith: 'specify what login type you want to use' 137 | , validLoginTypes: 'specifies the different login types available and associated behavior' 138 | , loginView: 'Either (A) the name of the view (e.g., "login.jade") or (B) the HTML string ' + 139 | 'that corresponds to the login page OR (C) a function (errors, login) {...} that returns the HTML string incorporating the array of `errors` messages and the `login` used in the prior attempt' 140 | , loginLayout: 'the name or path to the layout you want to use for your login' 141 | , loginLocals: 'Configure extra locals to pass to your login view' 142 | , loginSuccessRedirect: 'The path we redirect to after a successful login.' 143 | , registerView: 'Either the name of the view (e.g., "register.jade") or the HTML string ' + 144 | 'that corresponds to the register page.' 145 | , registerLayout: 'the name or path to the layout you want to use for your registration page' 146 | , registerLocals: 'Configure extra locals to pass to your register/registration view' 147 | , registerSuccessRedirect: 'The path we redirect to after a successful registration.' 148 | }) 149 | .loginFormFieldName('login') 150 | .passwordFormFieldName('password') 151 | .loginHumanName('login') 152 | .loginKey('login') 153 | 154 | .get('getLoginPath', "the login page's uri path.") 155 | .step('displayLogin') 156 | .accepts('req res') 157 | .promises(null) 158 | .displayLogin(renderLogin) 159 | 160 | .post('postLoginPath', "the uri path that the login POSTs to. Same as the 'action' field of the login
.") 161 | .step('extractLoginPassword') 162 | .accepts('req res') 163 | .promises('login password') 164 | .step('authenticate') 165 | .accepts('login password req res') 166 | .promises('userOrErrors') 167 | .step('interpretUserOrErrors') 168 | .description('Pipes the output of the step `authenticate` into either the `user` or `errors` param') 169 | .accepts('userOrErrors') 170 | .promises('user errors') 171 | .step('getSession') 172 | .description('Retrieves the session of the incoming request and returns in') 173 | .accepts('req') 174 | .promises('session') 175 | .step('addToSession') 176 | .description('Adds the user to the session') 177 | .accepts('session user errors') 178 | .promises(null) 179 | .step('respondToLoginSucceed') // TODO Rename to maybeRespondToLoginSucceed ? 180 | .description('Execute a HTTP response for a successful login') 181 | .accepts('res user') 182 | .promises(null) 183 | .step('respondToLoginFail') 184 | .description('Execute a HTTP response for a failed login') 185 | .accepts('req res errors login') 186 | .promises(null) 187 | .extractLoginPassword( function (req, res) { 188 | return [req.body[this.loginFormFieldName()], req.body[this.passwordFormFieldName()]]; 189 | }) 190 | .interpretUserOrErrors( function (userOrErrors) { 191 | if (Array.isArray(userOrErrors)) { 192 | return [null, userOrErrors]; // We have an array of errors 193 | } else { 194 | return [userOrErrors, []]; // We have a user 195 | } 196 | }) 197 | .getSession( function (req) { 198 | return req.session; 199 | }) 200 | .addToSession( function (sess, user, errors) { 201 | var _auth = sess.auth || (sess.auth = {}); 202 | if (user) 203 | _auth.userId = user.id; 204 | _auth.loggedIn = !!user; 205 | }) 206 | .respondToLoginSucceed( function (res, user) { 207 | if (user) { 208 | res.writeHead(303, {'Location': this.loginSuccessRedirect()}); 209 | res.end(); 210 | } 211 | }) 212 | .respondToLoginFail( function (req, res, errors, login) { 213 | if (!errors || !errors.length) return; 214 | return renderLoginFail.apply(this, arguments); 215 | }) 216 | 217 | .get('getRegisterPath', "the registration page's uri path.") 218 | .step('displayRegister') 219 | .accepts('req res') 220 | .promises(null) 221 | .displayRegister( function (req, res) { 222 | return renderRegister.apply(this, arguments); 223 | }) 224 | 225 | .post('postRegisterPath', "the uri path that the registration POSTs to. Same as the 'action' field of the registration .") 226 | .step('extractLoginPassword') // Re-used (/search for other occurence) 227 | .step('extractExtraRegistrationParams') 228 | .description('Extracts additonal query or body params from the ' + 229 | 'incoming request and returns them in the `extraParams` object') 230 | .accepts('req') 231 | .promises('extraParams') 232 | .step('aggregateParams') 233 | .description('Combines login, password, and extraParams into a newUserAttributes Object containing everything in extraParams plus login and password key/value pairs') 234 | .accepts('login password extraParams') 235 | .promises('newUserAttributes') 236 | .step('validateRegistrationBase') 237 | .description("Basic validation done by `everyauth`. Don't edit this. Configure validateRegistration instead.") 238 | .accepts('newUserAttributes') 239 | .promises('baseErrors') 240 | .step('validateRegistration') 241 | .description('Validates the registration parameters. Default includes check for existing user') 242 | .accepts('newUserAttributes baseErrors') 243 | .promises('errors') 244 | .step('maybeBreakToRegistrationFailSteps') 245 | .accepts('req res errors newUserAttributes') 246 | .promises(null) 247 | .canBreakTo('registrationFailSteps') 248 | .step('registerUser') 249 | .description('Creates and returns a new user with newUserAttributes') 250 | .accepts('newUserAttributes') 251 | .promises('userOrErrors') 252 | .step('extractUserOrHandleErrors') // User registration may throw an error if DB detects a non-unique value for login 253 | .accepts('req res userOrErrors newUserAttributes') 254 | .promises('user') 255 | .canBreakTo('registrationFailSteps') 256 | .step('getSession') 257 | .step('addToSession') 258 | .step('respondToRegistrationSucceed') 259 | .accepts('res user') 260 | .promises(null) 261 | .extractExtraRegistrationParams( function (req) { 262 | return {}; 263 | }) 264 | .aggregateParams( function (login, password, extraParams) { 265 | var params = extraParams; 266 | params[this.loginKey()] = login; 267 | params.password = password; 268 | return params; 269 | }) 270 | .validateRegistrationBase( function (newUserAttributes) { 271 | var loginWith = this.loginWith() 272 | , loginWithSpec, loginWithSanitize, loginWithValidate 273 | , login = newUserAttributes[this.loginKey()] 274 | , password = newUserAttributes.password 275 | , errors = []; 276 | if (!login) { 277 | errors.push('Missing ' + this.loginHumanName()); 278 | } else { 279 | // loginWith specific validations 280 | validLoginTypes = this.validLoginTypes(); 281 | loginWithSpec = this.validLoginTypes()[loginWith]; 282 | 283 | // Sanitize first 284 | loginWithSanitize = loginWithSpec.sanitize; 285 | if (loginWithSanitize) { 286 | login = loginWithSanitize(login); 287 | } 288 | 289 | // Validate second 290 | validateLoginWith = loginWithSpec.validate; 291 | if (validateLoginWith) { 292 | if (!validateLoginWith(login)) { 293 | // Add error third 294 | errors.push(loginWithSpec.error); 295 | } 296 | } 297 | } 298 | if (!password) errors.push('Missing password'); 299 | return errors; 300 | }) 301 | .validateRegistration( function (newUserAttributes, baseErrors) { 302 | return baseErrors; 303 | }) 304 | .maybeBreakToRegistrationFailSteps( function (req, res, errors, newUserAttributes) { 305 | var user, loginField, loginKey; 306 | if (errors && errors.length) { 307 | user = newUserAttributes; 308 | loginField = this.loginFormFieldName(); 309 | loginKey = this.loginKey(); 310 | if (loginField !== loginKey) { 311 | user[loginField] = user[loginKey]; 312 | delete user[this.loginKey()]; 313 | } 314 | delete user.password; 315 | return this.breakTo('registrationFailSteps', req, res, errors, user); 316 | } 317 | }) 318 | .extractUserOrHandleErrors( function (req, res, userOrErrors, newUserAttributes) { 319 | var errors, user; 320 | if (Array.isArray(userOrErrors)) { 321 | errors = userOrErrors; 322 | user = newUserAttributes; 323 | delete user.password; 324 | return this.breakTo('registrationFailSteps', req, res, errors, user); 325 | } 326 | user = userOrErrors; 327 | return user; 328 | }) 329 | .respondToRegistrationSucceed( function (res, user) { 330 | res.writeHead(303, {'Location': this.registerSuccessRedirect()}); 331 | res.end(); 332 | }) 333 | 334 | .stepseq('registrationFailSteps') 335 | .step('respondToRegistrationFail') 336 | .accepts('req res errors newUserAttributes') 337 | .promises(null) 338 | .respondToRegistrationFail( function (req, res, errors, newUserAttributes) { 339 | return renderRegisterFail.apply(this, arguments); 340 | }); 341 | 342 | function validateEmail (value) { 343 | // From Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ 344 | var isValid = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value); 345 | 346 | return isValid; 347 | } 348 | 349 | password.validLoginTypes({ 350 | login: {} 351 | , email: { 352 | validate: validateEmail 353 | , error: 'Please correct your email.' 354 | } 355 | , phone: { 356 | sanitize: function (value) { 357 | // Pull out only +, digits and 'x' for extension 358 | return value.replace(/[^\+\dx]/g, ''); 359 | } 360 | , validate: function (value) { 361 | return value.length >= 7; 362 | } 363 | , error: 'Please correct your phone.' 364 | } 365 | }); 366 | 367 | password.loginWith = function (loginType) { 368 | if (!arguments.length) return this._loginType; 369 | 370 | this._loginType = loginType; 371 | var name 372 | , validLoginTypes = Object.keys(this.validLoginTypes()); 373 | if (-1 === validLoginTypes.indexOf(loginType)) { 374 | throw new Error("loginWith only supports " + validLoginTypes.join(', ')); 375 | } 376 | this.loginFormFieldName(loginType); 377 | this.loginKey(loginType); 378 | this.loginHumanName(loginType); 379 | return this; 380 | }; 381 | password.loginWith('login'); 382 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | everyauth 2 | ========== 3 | 4 | Authentication and authorization (password, facebook, & more) for your node.js Connect and Express apps. 5 | 6 | There is a NodeTuts screencast of everyauth [here](http://nodetuts.com/tutorials/26-starting-with-everyauth.html#video) 7 | 8 | So far, `everyauth` enables you to login via: 9 | 10 | - `password` 11 | - `OpenId`                                            (Credits [RocketLabs Development](https://github.com/rocketlabsdev), [Andrew Mee](https://github.com/starfishmod), [Brian Noguchi](https://github.com/bnoguchi)) 12 | - `Google Hybrid`                         (Credits [RocketLabs Development](https://github.com/rocketlabsdev)) 13 | - OAuth 14 | - `twitter` 15 | - `linkedin` 16 | - `yahoo` 17 | - `readability`                             (Credits [Alfred Nerstu](https://github.com/alfrednerstu)) 18 | - `dropbox`                                    (Credits [Torgeir](https://github.com/torgeir)) 19 | - `justin.tv`                                 (Credits [slickplaid](https://github.com/slickplaid)) 20 | - `vimeo`                                 (Credits [slickplaid](https://github.com/slickplaid)) 21 | - `tumblr` 22 | - OAuth2 23 | - `facebook` 24 | - `github` 25 | - `instagram` 26 | - `foursquare` 27 | - `google` 28 | - `gowalla`                                    (Credits [Andrew Kramolisch](https://github.com/andykram)) 29 | - `37signals` (Basecamp, Highrise, Backpack, Campfire) 30 | - `box` (Box.net) 31 | - `LDAP` (experimental; not production-tested) 32 | 33 | `everyauth` is: 34 | 35 | - **Modular** - We have you covered with Facebook and Twitter 36 | OAuth logins, basic login/password support, and modules 37 | coming soon for beta invitation support and more. 38 | - **Easily Configurable** - everyauth was built with powerful 39 | configuration needs in mind. Configure an authorization strategy 40 | in a straightforward, easy-to-read & easy-to-write approach, 41 | with as much granularity as you want over the steps and 42 | logic of your authorization strategy. 43 | - **Idiomatic** - The syntax for configuring and extending your authorization strategies are 44 | idiomatic and chainable. 45 | 46 | 47 | ## Installation 48 | $ npm install everyauth 49 | 50 | 51 | ## Quick Start 52 | Using everyauth comes down to just 2 simple steps if using Connect 53 | or 3 simple steps if using Express: 54 | 55 | 1. **Choose and Configure Auth Strategies** - Find the authentication strategy 56 | you desire in one of the sections below. Follow the configuration 57 | instructions. 58 | 2. **Add the Middleware to Connect** 59 | 60 | ```javascript 61 | var everyauth = require('everyauth'); 62 | // Step 1 code goes here 63 | 64 | // Step 2 code 65 | var connect = require('connect'); 66 | var app = connect( 67 | connect.bodyParser() 68 | , connect.cookieParser() 69 | , connect.session({secret: 'mr ripley'}) 70 | , everyauth.middleware() 71 | , connect.router(routes) 72 | ); 73 | ``` 74 | 3. **Add View Helpers to Express** 75 | 76 | ```javascript 77 | // Step 1 code 78 | // ... 79 | // Step 2 code 80 | // ... 81 | 82 | // Step 3 code 83 | everyauth.helpExpress(app); 84 | 85 | app.listen(3000); 86 | ``` 87 | 88 | For more about what view helpers `everyauth` adds to your app, see the section 89 | titled "Express Helpers" near the bottom of this README. 90 | 91 | ## Example Application 92 | 93 | There is an example application at [./example](https://github.com/bnoguchi/everyauth/tree/master/example) 94 | 95 | To run it: 96 | 97 | $ cd example 98 | $ node server.js 99 | 100 | **Important** - Some OAuth Providers do not allow callbacks to localhost, so you will need to create a `localhost` 101 | alias called `local.host`. Make sure you set up your /etc/hosts so that 127.0.0.1 is also 102 | associated with 'local.host'. So inside your /etc/hosts file, one of the lines will look like: 103 | 104 | 127.0.0.1 localhost local.host 105 | 106 | Then point your browser to [http://local.host:3000](http://local.host:3000) 107 | 108 | ## Tests 109 | 110 | First, spin up the example server (See last section "Example Application"). 111 | 112 | Then, 113 | 114 | $ make test 115 | 116 | ## Logging Out 117 | 118 | If you integrate `everyauth` with `connect`, then `everyauth` automatically 119 | sets up a `logoutPath` at `GET` `/logout` for your app. It also 120 | sets a default handler for your logout route that clears your session 121 | of auth information and redirects them to '/'. 122 | 123 | To over-write the logout path: 124 | 125 | ```javascript 126 | everyauth.everymodule.logoutPath('/bye'); 127 | ``` 128 | 129 | To over-write the logout redirect path: 130 | 131 | ```javascript 132 | everyauth.everymodule.logoutRedirectPath('/navigate/to/after/logout'); 133 | ``` 134 | 135 | To over-write the logout handler: 136 | 137 | ```javascript 138 | everyauth.everymodule.handleLogout( function (req, res) { 139 | // Put you extra logic here 140 | 141 | req.logout(); // The logout method is added for you by everyauth, too 142 | 143 | // And/or put your extra logic here 144 | 145 | res.writeHead(303, { 'Location': this.logoutRedirectPath() }); 146 | res.end(); 147 | }); 148 | ``` 149 | 150 | ## Setting up Facebook Connect 151 | 152 | ```javascript 153 | var everyauth = require('everyauth') 154 | , connect = require('connect'); 155 | 156 | everyauth.facebook 157 | .appId('YOUR APP ID HERE') 158 | .appSecret('YOUR APP SECRET HERE') 159 | .handleAuthCallbackError( function (req, res) { 160 | // If a user denies your app, Facebook will redirect the user to 161 | // /auth/facebook/callback?error_reason=user_denied&error=access_denied&error_description=The+user+denied+your+request. 162 | // This configurable route handler defines how you want to respond to 163 | // that. 164 | // If you do not configure this, everyauth renders a default fallback 165 | // view notifying the user that their authentication failed and why. 166 | }) 167 | .findOrCreateUser( function (session, accessToken, accessTokExtra, fbUserMetadata) { 168 | // find or create user logic goes here 169 | }) 170 | .redirectPath('/'); 171 | 172 | var routes = function (app) { 173 | // Define your routes here 174 | }; 175 | 176 | connect( 177 | connect.bodyParser() 178 | , connect.cookieParser() 179 | , connect.session({secret: 'whodunnit'}) 180 | , everyauth.middleware() 181 | , connect.router(routes); 182 | ).listen(3000); 183 | ``` 184 | 185 | You can also configure more parameters (most are set to defaults) via 186 | the same chainable API: 187 | 188 | ```javascript 189 | everyauth.facebook 190 | .entryPath('/auth/facebook') 191 | .callbackPath('/auth/facebook/callback') 192 | .scope('email') // Defaults to undefined 193 | ``` 194 | 195 | If you want to see what the current value of a 196 | configured parameter is, you can do so via: 197 | 198 | ```javascript 199 | everyauth.facebook.scope(); // undefined 200 | everyauth.facebook.entryPath(); // '/auth/facebook' 201 | ``` 202 | 203 | To see all parameters that are configurable, the following will return an 204 | object whose parameter name keys map to description values: 205 | 206 | ```javascript 207 | everyauth.facebook.configurable(); 208 | ``` 209 | 210 | #### Dynamic Facebook Connect Scope 211 | 212 | Facebook provides many different 213 | [permissions](http://developers.facebook.com/docs/authentication/permissions/) 214 | for which your app can ask your user. This is bundled up in the `scope` query 215 | paremter sent with the oauth request to Facebook. While your app may require 216 | several different permissions from Facebook, Facebook recommends that you only 217 | ask for these permissions incrementally, as you need them. For example, you might 218 | want to only ask for the "email" scope upon registration. At the same time, for 219 | another user, you may want to ask for "user_status" permissions because they 220 | have progressed further along in your application. 221 | 222 | `everyauth` enables you to specify the "scope" dynamically with a second 223 | variation of the configurable `scope`. In addition to the first variation 224 | that looks like: 225 | 226 | ```javascript 227 | everyauth.facebook 228 | .scope('email,user_status'); 229 | ``` 230 | 231 | you can have greater dynamic control over "scope" via the second variation of `scope`: 232 | 233 | ```javascript 234 | everyauth.facebook 235 | .scope( function (req, res) { 236 | var session = req.session; 237 | switch (session.userPhase) { 238 | case 'registration': 239 | return 'email'; 240 | case 'share-media': 241 | return 'email,user_status'; 242 | } 243 | }); 244 | 245 | ``` 246 | 247 | ## Setting up Twitter OAuth 248 | 249 | ```javascript 250 | var everyauth = require('everyauth') 251 | , connect = require('connect'); 252 | 253 | everyauth.twitter 254 | .consumerKey('YOUR CONSUMER ID HERE') 255 | .consumerSecret('YOUR CONSUMER SECRET HERE') 256 | .findOrCreateUser( function (session, accessToken, accessTokenSecret, twitterUserMetadata) { 257 | // find or create user logic goes here 258 | }) 259 | .redirectPath('/'); 260 | 261 | var routes = function (app) { 262 | // Define your routes here 263 | }; 264 | 265 | connect( 266 | connect.bodyParser() 267 | , connect.cookieParser() 268 | , connect.session({secret: 'whodunnit'}) 269 | , everyauth.middleware() 270 | , connect.router(routes); 271 | ).listen(3000); 272 | ``` 273 | 274 | **Important** - Some developers forget to do the following, and it causes them to have issues with `everyauth`. 275 | Please make sure to do the following: When you set up your app at http://dev.twitter.com/, make sure that your callback url is set up to 276 | include that path '/auth/twitter/callback/'. In general, when dealing with OAuth or OAuth2 modules 277 | provided by `everyauth`, the default callback path is always set up to follow the pattern 278 | '/auth/#{moduleName}/callback', so just ensure that you configure your OAuth settings accordingly with 279 | the OAuth provider -- in this case, the "Edit Application Settings" section for your app at http://dev.twitter.com. 280 | 281 | Alternatively, you can specify the callback url at the application level by configuring `callbackPath` (which 282 | has a default configuration of "/auth/twitter/callback"): 283 | 284 | ```javascript 285 | everyauth.twitter 286 | .consumerKey('YOUR CONSUMER ID HERE') 287 | .consumerSecret('YOUR CONSUMER SECRET HERE') 288 | .callbackPath('/custom/twitter/callback/path') 289 | .findOrCreateUser( function (session, accessToken, accessTokenSecret, twitterUserMetadata) { 290 | // find or create user logic goes here 291 | }) 292 | .redirectPath('/'); 293 | ``` 294 | 295 | So if your hostname is `example.com`, then this configuration will over-ride the `dev.twitter.com` callback url configuration. 296 | Instead, Twitter will redirect back to `example.com/custom/twitter/callback/path` in the example just given above. 297 | 298 | You can also configure more parameters (most are set to defaults) via 299 | the same chainable API: 300 | 301 | ```javascript 302 | everyauth.twitter 303 | .entryPath('/auth/twitter') 304 | .callbackPath('/auth/twitter/callback'); 305 | ``` 306 | 307 | If you want to see what the current value of a 308 | configured parameter is, you can do so via: 309 | 310 | ```javascript 311 | everyauth.twitter.callbackPath(); // '/auth/twitter/callback' 312 | everyauth.twitter.entryPath(); // '/auth/twitter' 313 | ``` 314 | 315 | To see all parameters that are configurable, the following will return an 316 | object whose parameter name keys map to description values: 317 | 318 | ```javascript 319 | everyauth.twitter.configurable(); 320 | ``` 321 | 322 | ## Setting up Password Authentication 323 | 324 | ```javascript 325 | var everyauth = require('everyauth') 326 | , connect = require('connect'); 327 | 328 | everyauth.password 329 | .getLoginPath('/login') // Uri path to the login page 330 | .postLoginPath('/login') // Uri path that your login form POSTs to 331 | .loginView('a string of html; OR the name of the jade/etc-view-engine view') 332 | .authenticate( function (login, password) { 333 | // Either, we return a user or an array of errors if doing sync auth. 334 | // Or, we return a Promise that can fulfill to promise.fulfill(user) or promise.fulfill(errors) 335 | // `errors` is an array of error message strings 336 | // 337 | // e.g., 338 | // Example 1 - Sync Example 339 | // if (usersByLogin[login] && usersByLogin[login].password === password) { 340 | // return usersByLogin[login]; 341 | // } else { 342 | // return ['Login failed']; 343 | // } 344 | // 345 | // Example 2 - Async Example 346 | // var promise = this.Promise() 347 | // YourUserModel.find({ login: login}, function (err, user) { 348 | // if (err) return promise.fulfill([err]); 349 | // promise.fulfill(user); 350 | // } 351 | // return promise; 352 | }) 353 | .loginSuccessRedirect('/') // Where to redirect to after a login 354 | 355 | // If login fails, we render the errors via the login view template, 356 | // so just make sure your loginView() template incorporates an `errors` local. 357 | // See './example/views/login.jade' 358 | 359 | .getRegisterPath('/register') // Uri path to the registration page 360 | .postRegisterPath('/register') // The Uri path that your registration form POSTs to 361 | .registerView('a string of html; OR the name of the jade/etc-view-engine view') 362 | .validateRegistration( function (newUserAttributes) { 363 | // Validate the registration input 364 | // Return undefined, null, or [] if validation succeeds 365 | // Return an array of error messages (or Promise promising this array) 366 | // if validation fails 367 | // 368 | // e.g., assuming you define validate with the following signature 369 | // var errors = validate(login, password, extraParams); 370 | // return errors; 371 | // 372 | // The `errors` you return show up as an `errors` local in your jade template 373 | }) 374 | .registerUser( function (newUserAttributes) { 375 | // This step is only executed if we pass the validateRegistration step without 376 | // any errors. 377 | // 378 | // Returns a user (or a Promise that promises a user) after adding it to 379 | // some user store. 380 | // 381 | // As an edge case, sometimes your database may make you aware of violation 382 | // of the unique login index, so if this error is sent back in an async 383 | // callback, then you can just return that error as a single element array 384 | // containing just that error message, and everyauth will automatically handle 385 | // that as a failed registration. Again, you will have access to this error via 386 | // the `errors` local in your register view jade template. 387 | // e.g., 388 | // var promise = this.Promise(); 389 | // User.create(newUserAttributes, function (err, user) { 390 | // if (err) return promise.fulfill([err]); 391 | // promise.fulfill(user); 392 | // }); 393 | // return promise; 394 | // 395 | // Note: Index and db-driven validations are the only validations that occur 396 | // here; all other validations occur in the `validateRegistration` step documented above. 397 | }) 398 | .registerSuccessRedirect('/'); // Where to redirect to after a successful registration 399 | 400 | var routes = function (app) { 401 | // Define your routes here 402 | }; 403 | 404 | connect( 405 | connect.bodyParser() 406 | , connect.cookieParser() 407 | , connect.session({secret: 'whodunnit'}) 408 | , everyauth.middleware() 409 | , connect.router(routes); 410 | ).listen(3000); 411 | ``` 412 | 413 | You can also configure more parameters (most are set to defaults) via 414 | the same chainable API: 415 | 416 | ```javascript 417 | everyauth.password 418 | .loginFormFieldName('login') // Defaults to 'login' 419 | .passwordFormFieldName('password') // Defaults to 'password' 420 | .loginLayout('custom_login_layout') // Only with `express` 421 | .registerLayout('custom reg_layout') // Only with `express` 422 | .loginLocals(fn); // See Recipe 3 below 423 | ``` 424 | 425 | If you want to see what the current value of a 426 | configured parameter is, you can do so via: 427 | 428 | ```javascript 429 | everyauth.password.loginFormFieldName(); // 'login' 430 | everyauth.password.passwordFormFieldName(); // 'password' 431 | ``` 432 | 433 | To see all parameters that are configurable, the following will return an 434 | object whose parameter name keys map to description values: 435 | 436 | ```javascript 437 | everyauth.password.configurable(); 438 | ``` 439 | 440 | ### Password Recipe 1: Extra registration data besides login + password 441 | 442 | Sometimes your registration will ask for more information from the user besides the login and password. 443 | 444 | For this particular scenario, you can configure the optional step, `extractExtraRegistrationParams`. 445 | 446 | ```javascript 447 | everyauth.password.extractExtraRegistrationParams( function (req) { 448 | return { 449 | phone: req.body.phone 450 | , name: { 451 | first: req.body.first_name 452 | , last: req.body.last_name 453 | } 454 | }; 455 | }); 456 | ``` 457 | 458 | ### Password Recipe 2: Logging in with email or phone number 459 | 460 | By default, `everyauth` uses the field and user key name `login` during the 461 | registration and login process. 462 | 463 | Sometimes, you want to use `email` or `phone` instead of `login`. Moreover, 464 | you also want to validate `email` and `phone` fields upon registration. 465 | 466 | `everyauth` provides an easy way to do this: 467 | 468 | ```javascript 469 | everyauth.password.loginWith('email'); 470 | 471 | // OR 472 | 473 | everyauth.password.loginWith('phone'); 474 | ``` 475 | 476 | With simple login configuration like this, you get email (or phone) validation 477 | in addition to renaming of the form field and user key corresponding to what 478 | otherwise would typically be referred to as 'login'. 479 | 480 | ### Password Recipe 3: Adding additional view local variables to login and registration views 481 | 482 | If you are using `express`, you are able to pass variables from your app 483 | context to your view context via local variables. `everyauth` provides 484 | several convenience local vars for your views, but sometimes you will want 485 | to augment this set of local vars with additional locals. 486 | 487 | So `everyauth` also provides a mechanism for you to do so via the following 488 | configurables: 489 | 490 | ```javascript 491 | everyauth.password.loginLocals(...); 492 | everyauth.password.registerLocals(...); 493 | ``` 494 | 495 | `loginLocals` and `registerLocals` configuration have symmetrical APIs, so I 496 | will only cover `loginLocals` here to illustrate how to use both. 497 | 498 | You can configure this parameter in one of *3* ways. Why 3? Because there are 3 types of ways that you can retrieve your locals. 499 | 500 | 1. Static local vars that never change values: 501 | 502 | ```javascript 503 | everyauth.password.loginLocals({ 504 | title: 'Login' 505 | }); 506 | ``` 507 | 2. Dynamic synchronous local vars that depend on the incoming request, but whose values are retrieved synchronously 508 | 509 | ```javascript 510 | everyauth.password.loginLocals( function (req, res) { 511 | var sess = req.session; 512 | return { 513 | isReturning: sess.isReturning 514 | }; 515 | }); 516 | ``` 517 | 3. Dynamic asynchronous local vars 518 | 519 | ```javascript 520 | everyauth.password.loginLocals( function (req, res, done) { 521 | asyncCall( function ( err, data) { 522 | if (err) return done(err); 523 | done(null, { 524 | title: il8n.titleInLanguage('Login Page', il8n.language(data.geo)) 525 | }); 526 | }); 527 | }); 528 | ``` 529 | 530 | ### Password Recipe 4: Customize Your Registration Validation 531 | 532 | By default, `everyauth.password` automatically 533 | 534 | - validates that the login (or email or phone, depending on what you authenticate with -- see Password Recipe 2) is present in the login http request, 535 | - validates that the password is present 536 | - validates that an email login is a correctly formatted email 537 | - validates that a phone login is a valid phone number 538 | 539 | If any of these validations fail, then the appropriate errors are generated and accessible to you in your view via the `errors` view local variable. 540 | 541 | If you want to add additional validations beyond this, you can do so by configuring the step, `validateRegistration`: 542 | 543 | ```javascript 544 | everyauth.password 545 | .validateRegistration( function (newUserAttributes, baseErrors) { 546 | // Here, newUserAttributes is the hash of parameters extracted from the incoming request. 547 | // baseErrors is the array of errors generated by the default automatic validation outlined above 548 | // in this same recipe. 549 | 550 | // First, validate your errors. Here, validateUser is a made up function 551 | var moreErrors = validateUser( newUserAttributes ); 552 | if (moreErrors.length) baseErrors.push.apply(baseErrors, moreErrors); 553 | 554 | // Return the array of errors, so your view has access to them. 555 | return baseErrors; 556 | }); 557 | ``` 558 | 559 | ## Setting up GitHub OAuth 560 | 561 | ```javascript 562 | var everyauth = require('everyauth') 563 | , connect = require('connect'); 564 | 565 | everyauth.github 566 | .appId('YOUR CLIENT ID HERE') 567 | .appSecret('YOUR CLIENT SECRET HERE') 568 | .findOrCreateUser( function (session, accessToken, , accessTokenExtra, githubUserMetadata) { 569 | // find or create user logic goes here 570 | }) 571 | .redirectPath('/'); 572 | 573 | var routes = function (app) { 574 | // Define your routes here 575 | }; 576 | 577 | connect( 578 | connect.bodyParser() 579 | , connect.cookieParser() 580 | , connect.session({secret: 'whodunnit'}) 581 | , everyauth.middleware() 582 | , connect.router(routes); 583 | ).listen(3000); 584 | ``` 585 | 586 | You can also configure more parameters (most are set to defaults) via 587 | the same chainable API: 588 | 589 | ```javascript 590 | everyauth.github 591 | .entryPath('/auth/github') 592 | .callbackPath('/auth/github/callback') 593 | .scope('repo'); // Defaults to undefined 594 | // Can be set to a combination of: 'user', 'public_repo', 'repo', 'gist' 595 | // For more details, see http://develop.github.com/p/oauth.html 596 | ``` 597 | 598 | If you want to see what the current value of a 599 | configured parameter is, you can do so via: 600 | 601 | ```javascript 602 | everyauth.github.scope(); // undefined 603 | everyauth.github.entryPath(); // '/auth/github' 604 | ``` 605 | 606 | To see all parameters that are configurable, the following will return an 607 | object whose parameter name keys map to description values: 608 | 609 | ```javascript 610 | everyauth.github.configurable(); 611 | ``` 612 | 613 | ## Setting up Instagram OAuth 614 | 615 | ```javascript 616 | var everyauth = require('everyauth') 617 | , connect = require('connect'); 618 | 619 | everyauth.instagram 620 | .appId('YOUR CLIENT ID HERE') 621 | .appSecret('YOUR CLIENT SECRET HERE') 622 | .findOrCreateUser( function (session, accessToken, accessTokenExtra, instagramUserMetadata) { 623 | // find or create user logic goes here 624 | }) 625 | .redirectPath('/'); 626 | 627 | var routes = function (app) { 628 | // Define your routes here 629 | }; 630 | 631 | connect( 632 | connect.bodyParser() 633 | , connect.cookieParser() 634 | , connect.session({secret: 'whodunnit'}) 635 | , everyauth.middleware() 636 | , connect.router(routes); 637 | ).listen(3000); 638 | ``` 639 | 640 | You can also configure more parameters (most are set to defaults) via 641 | the same chainable API: 642 | 643 | ```javascript 644 | everyauth.instagram 645 | .entryPath('/auth/instagram') 646 | .callbackPath('/auth/instagram/callback') 647 | .scope('basic') // Defaults to 'basic' 648 | // Can be set to a combination of: 'basic', 'comments', 'relationships', 'likes' 649 | // For more details, see http://instagram.com/developer/auth/#scope 650 | .display(undefined); // Defaults to undefined; Set to 'touch' to see a mobile optimized version 651 | // of the instagram auth page 652 | ``` 653 | 654 | If you want to see what the current value of a 655 | configured parameter is, you can do so via: 656 | 657 | ```javascript 658 | everyauth.instagram.callbackPath(); // '/auth/instagram/callback' 659 | everyauth.instagram.entryPath(); // '/auth/instagram' 660 | ``` 661 | 662 | To see all parameters that are configurable, the following will return an 663 | object whose parameter name keys map to description values: 664 | 665 | ```javascript 666 | everyauth.instagram.configurable(); 667 | ``` 668 | 669 | ## Setting up Foursquare OAuth 670 | 671 | ```javascript 672 | var everyauth = require('everyauth') 673 | , connect = require('connect'); 674 | 675 | everyauth.foursquare 676 | .appId('YOUR CLIENT ID HERE') 677 | .appSecret('YOUR CLIENT SECRET HERE') 678 | .findOrCreateUser( function (session, accessToken, accessTokenExtra, foursquareUserMetadata) { 679 | // find or create user logic goes here 680 | }) 681 | .redirectPath('/'); 682 | 683 | var routes = function (app) { 684 | // Define your routes here 685 | }; 686 | 687 | connect( 688 | connect.bodyParser() 689 | , connect.cookieParser() 690 | , connect.session({secret: 'whodunnit'}) 691 | , everyauth.middleware() 692 | , connect.router(routes); 693 | ).listen(3000); 694 | ``` 695 | 696 | You can also configure more parameters (most are set to defaults) via 697 | the same chainable API: 698 | 699 | ```javascript 700 | everyauth.foursquare 701 | .entryPath('/auth/foursquare') 702 | .callbackPath('/auth/foursquare/callback'); 703 | ``` 704 | 705 | If you want to see what the current value of a 706 | configured parameter is, you can do so via: 707 | 708 | ```javascript 709 | everyauth.foursquare.callbackPath(); // '/auth/foursquare/callback' 710 | everyauth.foursquare.entryPath(); // '/auth/foursquare' 711 | ``` 712 | 713 | To see all parameters that are configurable, the following will return an 714 | object whose parameter name keys map to description values: 715 | 716 | ```javascript 717 | everyauth.foursquare.configurable(); 718 | ``` 719 | 720 | ## Setting up LinkedIn OAuth 721 | 722 | ```javascript 723 | var everyauth = require('everyauth') 724 | , connect = require('connect'); 725 | 726 | everyauth.linkedin 727 | .consumerKey('YOUR CONSUMER ID HERE') 728 | .consumerSecret('YOUR CONSUMER SECRET HERE') 729 | .findOrCreateUser( function (session, accessToken, accessTokenSecret, linkedinUserMetadata) { 730 | // find or create user logic goes here 731 | }) 732 | .redirectPath('/'); 733 | 734 | var routes = function (app) { 735 | // Define your routes here 736 | }; 737 | 738 | connect( 739 | connect.bodyParser() 740 | , connect.cookieParser() 741 | , connect.session({secret: 'whodunnit'}) 742 | , everyauth.middleware() 743 | , connect.router(routes); 744 | ).listen(3000); 745 | ``` 746 | 747 | You can also configure more parameters (most are set to defaults) via 748 | the same chainable API: 749 | 750 | ```javascript 751 | everyauth.linkedin 752 | .entryPath('/auth/linkedin') 753 | .callbackPath('/auth/linkedin/callback'); 754 | ``` 755 | 756 | If you want to see what the current value of a 757 | configured parameter is, you can do so via: 758 | 759 | ```javascript 760 | everyauth.linkedin.callbackPath(); // '/auth/linkedin/callback' 761 | everyauth.linkedin.entryPath(); // '/auth/linkedin' 762 | ``` 763 | 764 | To see all parameters that are configurable, the following will return an 765 | object whose parameter name keys map to description values: 766 | 767 | ```javascript 768 | everyauth.linkedin.configurable(); 769 | ``` 770 | 771 | ## Setting up Google OAuth2 772 | 773 | ```javascript 774 | var everyauth = require('everyauth') 775 | , connect = require('connect'); 776 | 777 | everyauth.google 778 | .appId('YOUR CLIENT ID HERE') 779 | .appSecret('YOUR CLIENT SECRET HERE') 780 | .scope('https://www.google.com/m8/feeds') // What you want access to 781 | .handleAuthCallbackError( function (req, res) { 782 | // If a user denies your app, Google will redirect the user to 783 | // /auth/facebook/callback?error=access_denied 784 | // This configurable route handler defines how you want to respond to 785 | // that. 786 | // If you do not configure this, everyauth renders a default fallback 787 | // view notifying the user that their authentication failed and why. 788 | }) 789 | .findOrCreateUser( function (session, accessToken, accessTokenExtra, googleUserMetadata) { 790 | // find or create user logic goes here 791 | // Return a user or Promise that promises a user 792 | // Promises are created via 793 | // var promise = this.Promise(); 794 | }) 795 | .redirectPath('/'); 796 | 797 | var routes = function (app) { 798 | // Define your routes here 799 | }; 800 | 801 | connect( 802 | connect.bodyParser() 803 | , connect.cookieParser() 804 | , connect.session({secret: 'whodunnit'}) 805 | , everyauth.middleware() 806 | , connect.router(routes); 807 | ).listen(3000); 808 | ``` 809 | 810 | You can also configure more parameters (most are set to defaults) via 811 | the same chainable API: 812 | 813 | ```javascript 814 | everyauth.google 815 | .entryPath('/auth/google') 816 | .callbackPath('/auth/google/callback'); 817 | ``` 818 | 819 | If you want to see what the current value of a 820 | configured parameter is, you can do so via: 821 | 822 | ```javascript 823 | everyauth.google.scope(); // undefined 824 | everyauth.google.entryPath(); // '/auth/google' 825 | ``` 826 | 827 | To see all parameters that are configurable, the following will return an 828 | object whose parameter name keys map to description values: 829 | 830 | ```javascript 831 | everyauth.google.configurable(); 832 | ``` 833 | 834 | ## Setting up Gowalla OAuth2 835 | 836 | ```javascript 837 | var everyauth = require('everyauth') 838 | , connect = require('connect'); 839 | 840 | everyauth.gowalla 841 | .appId('YOUR CLIENT ID HERE') 842 | .appSecret('YOUR CLIENT SECRET HERE') 843 | .handleAuthCallbackError( function (req, res) { 844 | // TODO - Update this documentation 845 | // This configurable route handler defines how you want to respond to 846 | // a response from Gowalla that something went wrong during the oauth2 process. 847 | // If you do not configure this, everyauth renders a default fallback 848 | // view notifying the user that their authentication failed and why. 849 | }) 850 | .findOrCreateUser( function (session, accessToken, accessTokenExtra, gowallaUserMetadata) { 851 | // find or create user logic goes here 852 | // Return a user or Promise that promises a user 853 | // Promises are created via 854 | // var promise = this.Promise(); 855 | }) 856 | .redirectPath('/'); 857 | 858 | var routes = function (app) { 859 | // Define your routes here 860 | }; 861 | 862 | connect( 863 | connect.bodyParser() 864 | , connect.cookieParser() 865 | , connect.session({secret: 'whodunnit'}) 866 | , everyauth.middleware() 867 | , connect.router(routes); 868 | ).listen(3000); 869 | ``` 870 | 871 | You can also configure more parameters (most are set to defaults) via 872 | the same chainable API: 873 | 874 | ```javascript 875 | everyauth.gowalla 876 | .entryPath('/auth/gowalla') 877 | .callbackPath('/auth/gowalla/callback'); 878 | ``` 879 | 880 | If you want to see what the current value of a 881 | configured parameter is, you can do so via: 882 | 883 | ```javascript 884 | everyauth.gowalla.scope(); // undefined 885 | everyauth.gowalla.entryPath(); // '/auth/gowalla' 886 | ``` 887 | 888 | To see all parameters that are configurable, the following will return an 889 | object whose parameter name keys map to description values: 890 | 891 | ```javascript 892 | everyauth.gowalla.configurable(); 893 | ``` 894 | 895 | ## Setting up 37signals (Basecamp, Highrise, Backpack, Campfire) OAuth2 896 | 897 | First, register an app at [integrate.37signals.com](https://integrate.37signals.com). 898 | 899 | ```javascript 900 | var everyauth = require('everyauth') 901 | , connect = require('connect'); 902 | 903 | everyauth['37signals'] 904 | .appId('YOUR CLIENT ID HERE') 905 | .appSecret('YOUR CLIENT SECRET HERE') 906 | .handleAuthCallbackError( function (req, res) { 907 | // TODO - Update this documentation 908 | // This configurable route handler defines how you want to respond to 909 | // a response from 37signals that something went wrong during the oauth2 process. 910 | // If you do not configure this, everyauth renders a default fallback 911 | // view notifying the user that their authentication failed and why. 912 | }) 913 | .findOrCreateUser( function (session, accessToken, accessTokenExtra, _37signalsUserMetadata) { 914 | // find or create user logic goes here 915 | // Return a user or Promise that promises a user 916 | // Promises are created via 917 | // var promise = this.Promise(); 918 | }) 919 | .redirectPath('/'); 920 | 921 | var routes = function (app) { 922 | // Define your routes here 923 | }; 924 | 925 | connect( 926 | connect.bodyParser() 927 | , connect.cookieParser() 928 | , connect.session({secret: 'whodunnit'}) 929 | , everyauth.middleware() 930 | , connect.router(routes); 931 | ).listen(3000); 932 | ``` 933 | 934 | You can also configure more parameters (most are set to defaults) via 935 | the same chainable API: 936 | 937 | ```javascript 938 | everyauth['37signals'] 939 | .entryPath('/auth/37signals') 940 | .callbackPath('/auth/37signals/callback'); 941 | ``` 942 | 943 | If you want to see what the current value of a 944 | configured parameter is, you can do so via: 945 | 946 | ```javascript 947 | everyauth['37signals'].entryPath(); // '/auth/37signals' 948 | ``` 949 | 950 | To see all parameters that are configurable, the following will return an 951 | object whose parameter name keys map to description values: 952 | 953 | ```javascript 954 | everyauth['37signals'].configurable(); 955 | ``` 956 | 957 | ## Setting up Yahoo OAuth 958 | 959 | ```javascript 960 | var everyauth = require('everyauth') 961 | , connect = require('connect'); 962 | 963 | everyauth.yahoo 964 | .consumerKey('YOUR CONSUMER KEY HERE') 965 | .consumerSecret('YOUR CONSUMER SECRET HERE') 966 | .findOrCreateUser( function (session, accessToken, accessTokenSecret, yahooUserMetadata) { 967 | // find or create user logic goes here 968 | }) 969 | .redirectPath('/'); 970 | 971 | var routes = function (app) { 972 | // Define your routes here 973 | }; 974 | 975 | connect( 976 | connect.bodyParser() 977 | , connect.cookieParser() 978 | , connect.session({secret: 'whodunnit'}) 979 | , everyauth.middleware() 980 | , connect.router(routes); 981 | ).listen(3000); 982 | ``` 983 | 984 | You can also configure more parameters (most are set to defaults) via 985 | the same chainable API: 986 | 987 | ```javascript 988 | everyauth.yahoo 989 | .entryPath('/auth/yahoo') 990 | .callbackPath('/auth/yahoo/callback'); 991 | ``` 992 | 993 | If you want to see what the current value of a 994 | configured parameter is, you can do so via: 995 | 996 | ```javascript 997 | everyauth.yahoo.callbackPath(); // '/auth/yahoo/callback' 998 | everyauth.yahoo.entryPath(); // '/auth/yahoo' 999 | ``` 1000 | 1001 | To see all parameters that are configurable, the following will return an 1002 | object whose parameter name keys map to description values: 1003 | 1004 | ```javascript 1005 | everyauth.yahoo.configurable(); 1006 | ``` 1007 | 1008 | ## Setting up Readability OAuth 1009 | 1010 | ```javascript 1011 | var everyauth = require('everyauth') 1012 | , connect = require('connect'); 1013 | 1014 | everyauth.readability 1015 | .consumerKey('YOUR CONSUMER KEY HERE') 1016 | .consumerSecret('YOUR CONSUMER SECRET HERE') 1017 | .findOrCreateUser( function (sess, accessToken, accessSecret, reader) { 1018 | // find or create user logic goes here 1019 | // 1020 | // e.g., 1021 | // return usersByReadabilityId[reader.username] || (usersByReadabilityId[reader.username] = reader); 1022 | }) 1023 | .redirectPath('/'); 1024 | 1025 | var routes = function (app) { 1026 | // Define your routes here 1027 | }; 1028 | 1029 | connect( 1030 | connect.bodyParser() 1031 | , connect.cookieParser() 1032 | , connect.session({secret: 'whodunnit'}) 1033 | , everyauth.middleware() 1034 | , connect.router(routes); 1035 | ).listen(3000); 1036 | ``` 1037 | 1038 | You can also configure more parameters (most are set to defaults) via 1039 | the same chainable API: 1040 | 1041 | ```javascript 1042 | everyauth.readability 1043 | .entryPath('/auth/readability') 1044 | .callbackPath('/auth/readability/callback'); 1045 | ``` 1046 | 1047 | If you want to see what the current value of a 1048 | configured parameter is, you can do so via: 1049 | 1050 | ```javascript 1051 | everyauth.readability.callbackPath(); // '/auth/readability/callback' 1052 | everyauth.readability.entryPath(); // '/auth/readability' 1053 | ``` 1054 | 1055 | To see all parameters that are configurable, the following will return an 1056 | object whose parameter name keys map to description values: 1057 | 1058 | ```javascript 1059 | everyauth.readability.configurable(); 1060 | ``` 1061 | 1062 | ## Setting up Dropbox OAuth 1063 | 1064 | ```javascript 1065 | var everyauth = require('everyauth') 1066 | , connect = require('connect'); 1067 | 1068 | everyauth.dropbox 1069 | .consumerKey('YOUR CONSUMER KEY HERE') 1070 | .consumerSecret('YOUR CONSUMER SECRET HERE') 1071 | .findOrCreateUser( function (sess, accessToken, accessSecret, user) { 1072 | // find or create user logic goes here 1073 | // 1074 | // e.g., 1075 | // return usersByDropboxId[user.uid] || (usersByDropboxId[user.uid] = user); 1076 | }) 1077 | .redirectPath('/'); 1078 | 1079 | var routes = function (app) { 1080 | // Define your routes here 1081 | }; 1082 | 1083 | connect( 1084 | connect.bodyParser() 1085 | , connect.cookieParser() 1086 | , connect.session({secret: 'whodunnit'}) 1087 | , everyauth.middleware() 1088 | , connect.router(routes); 1089 | ).listen(3000); 1090 | ``` 1091 | 1092 | You can also configure more parameters (most are set to defaults) via 1093 | the same chainable API: 1094 | 1095 | ```javascript 1096 | everyauth.dropbox 1097 | .entryPath('/auth/dropbox') 1098 | .callbackPath('/auth/dropbox/callback'); 1099 | ``` 1100 | 1101 | If you want to see what the current value of a 1102 | configured parameter is, you can do so via: 1103 | 1104 | ```javascript 1105 | everyauth.dropbox.callbackPath(); // '/auth/dropbox/callback' 1106 | everyauth.dropbox.entryPath(); // '/auth/dropbox' 1107 | ``` 1108 | 1109 | To see all parameters that are configurable, the following will return an 1110 | object whose parameter name keys map to description values: 1111 | 1112 | ```javascript 1113 | everyauth.dropbox.configurable(); 1114 | ``` 1115 | 1116 | ## Setting up Justin.tv OAuth 1117 | 1118 | [Sign up for a Justin.tv account](http://www.justin.tv/user/signup) and activate it as a [developer account](http://www.justin.tv/developer/activate) to get your consumer key and secret. 1119 | 1120 | ```javascript 1121 | var everyauth = require('everyauth') 1122 | , connect = require('connect'); 1123 | 1124 | everyauth.justintv 1125 | .consumerKey('YOUR CONSUMER KEY HERE') 1126 | .consumerSecret('YOUR CONSUMER SECRET HERE') 1127 | .findOrCreateUser( function (sess, accessToken, accessSecret, justintvUser) { 1128 | // find or create user logic goes here 1129 | // 1130 | // e.g., 1131 | // return usersByJustintvId[justintvUser.id] || (usersByJustintvId[justintvUser.id] = justintvUser); 1132 | }) 1133 | .redirectPath('/'); 1134 | 1135 | var routes = function (app) { 1136 | // Define your routes here 1137 | }; 1138 | 1139 | connect( 1140 | connect.bodyParser() 1141 | , connect.cookieParser() 1142 | , connect.session({secret: 'whodunnit'}) 1143 | , everyauth.middleware() 1144 | , connect.router(routes); 1145 | ).listen(3000); 1146 | ``` 1147 | 1148 | The `justintvUser` parameter in the `.findOrCreateUser()` function above returns the `account/whoami` API call 1149 | 1150 | [Justin.tv API Wiki - Account/whoami](http://apiwiki.justin.tv/mediawiki/index.php/Account/whoami) 1151 | 1152 | ```javascript 1153 | { 1154 | "image_url_huge": "http:\/\/static-cdn.justin.tv\/jtv_user_pictures\/justin-320x240-4.jpg", 1155 | "profile_header_border_color": null, 1156 | "favorite_quotes": "I love Justin.tv", 1157 | "sex": "Male", 1158 | "image_url_large": "http:\/\/static-cdn.justin.tv\/jtv_user_pictures\/justin-125x94-4.jpg", 1159 | "profile_about": "Check out my website:\n\nwww.justin.tv\n", 1160 | "profile_background_color": null, 1161 | "image_url_medium": "http:\/\/static-cdn.justin.tv\/jtv_user_pictures\/justin-75x56-4.jpg", 1162 | "id": 1698, 1163 | "broadcaster": true, 1164 | "profile_url": "http:\/\/www.justin.tv\/justin\/profile", 1165 | "profile_link_color": null, 1166 | "image_url_small": "http:\/\/static-cdn.justin.tv\/jtv_user_pictures\/justin-50x37-4.jpg", 1167 | "profile_header_text_color": null, 1168 | "name": "The JUST UN", 1169 | "image_url_tiny": "http:\/\/static-cdn.justin.tv\/jtv_user_pictures\/justin-33x25-4.jpg", 1170 | "login": "justin", 1171 | "profile_header_bg_color": null, 1172 | "location": "San Francisco" 1173 | } 1174 | ``` 1175 | 1176 | You can also configure more parameters (most are set to defaults) via the same chainable API: 1177 | 1178 | ```javascript 1179 | everyauth.justintv 1180 | .entryPath('/auth/justintv') 1181 | .callbackPath('/auth/justintv/callback'); 1182 | ``` 1183 | 1184 | If you want to see what the current value of a configured parameter is, you can do so via: 1185 | 1186 | ```javascript 1187 | everyauth.justintv.callbackPath(); // '/auth/justintv/callback' 1188 | everyauth.justintv.entryPath(); // '/auth/justintv' 1189 | ``` 1190 | 1191 | To see all parameters that are configurable, the following will return an object whose parameter name keys map to description values: 1192 | 1193 | ```javascript 1194 | everyauth.justintv.configurable(); 1195 | ``` 1196 | 1197 | ## Setting up Vimeo OAuth 1198 | 1199 | You will first need to sign up for a [developer application](http://vimeo.com/api/applications) to get the consumer key and secret. 1200 | 1201 | ```javascript 1202 | var everyauth = require('everyauth') 1203 | , connect = require('connect'); 1204 | 1205 | everyauth.vimeo 1206 | .consumerKey('YOUR CONSUMER KEY HERE') 1207 | .consumerSecret('YOUR CONSUMER SECRET HERE') 1208 | .findOrCreateUser( function (sess, accessToken, accessSecret, user) { 1209 | // find or create user logic goes here 1210 | // 1211 | // e.g., 1212 | // return usersByVimeoId[user.id] || (usersByVimeoId[user.id] = user); 1213 | }) 1214 | .redirectPath('/'); 1215 | 1216 | var routes = function (app) { 1217 | // Define your routes here 1218 | }; 1219 | 1220 | connect( 1221 | connect.bodyParser() 1222 | , connect.cookieParser() 1223 | , connect.session({secret: 'whodunnit'}) 1224 | , everyauth.middleware() 1225 | , connect.router(routes); 1226 | ).listen(3000); 1227 | ``` 1228 | 1229 | You can also configure more parameters (most are set to defaults) via 1230 | the same chainable API: 1231 | 1232 | ```javascript 1233 | everyauth.vimeo 1234 | .entryPath('/auth/vimeo') 1235 | .callbackPath('/auth/vimeo/callback'); 1236 | ``` 1237 | 1238 | If you want to see what the current value of a 1239 | configured parameter is, you can do so via: 1240 | 1241 | ```javascript 1242 | everyauth.vimeo.callbackPath(); // '/auth/vimeo/callback' 1243 | everyauth.vimeo.entryPath(); // '/auth/vimeo' 1244 | ``` 1245 | 1246 | To see all parameters that are configurable, the following will return an 1247 | object whose parameter name keys map to description values: 1248 | 1249 | ```javascript 1250 | everyauth.vimeo.configurable(); 1251 | ``` 1252 | 1253 | ## Setting up Tumblr OAuth (1.a) 1254 | 1255 | You will first need to [register an app](http://www.tumblr.com/oauth/register) to get the consumer key and secret. 1256 | During registration of your new app, enter a "Default callback URL" of "http://:/auth/tumblr/callback". 1257 | Once you register your app, copy down your "OAuth Consumer Key" and "Secret Key" and proceed below. 1258 | 1259 | ```javascript 1260 | var everyauth = require('everyauth') 1261 | , connect = require('connect'); 1262 | 1263 | everyauth.tumblr 1264 | .consumerKey('YOUR CONSUMER KEY HERE') 1265 | .consumerSecret('YOUR CONSUMER SECRET HERE') 1266 | .findOrCreateUser( function (sess, accessToken, accessSecret, user) { 1267 | // find or create user logic goes here 1268 | // 1269 | // e.g., 1270 | // return usersByTumblrName[user.name] || (usersByTumblrName[user.name] = user); 1271 | }) 1272 | .redirectPath('/'); 1273 | 1274 | var routes = function (app) { 1275 | // Define your routes here 1276 | }; 1277 | 1278 | connect( 1279 | connect.bodyParser() 1280 | , connect.cookieParser() 1281 | , connect.session({secret: 'whodunnit'}) 1282 | , everyauth.middleware() 1283 | , connect.router(routes); 1284 | ).listen(3000); 1285 | ``` 1286 | 1287 | You can also configure more parameters (most are set to defaults) via 1288 | the same chainable API: 1289 | 1290 | ```javascript 1291 | everyauth.tumblr 1292 | .entryPath('/auth/tumblr') 1293 | .callbackPath('/auth/tumblr/callback'); 1294 | ``` 1295 | 1296 | If you want to see what the current value of a 1297 | configured parameter is, you can do so via: 1298 | 1299 | ```javascript 1300 | everyauth.tumblr.callbackPath(); // '/auth/tumblr/callback' 1301 | everyauth.tumblr.entryPath(); // '/auth/tumblr' 1302 | ``` 1303 | 1304 | To see all parameters that are configurable, the following will return an 1305 | object whose parameter name keys map to description values: 1306 | 1307 | ```javascript 1308 | everyauth.tumblr.configurable(); 1309 | ``` 1310 | 1311 | ## Setting up OpenID protocol 1312 | 1313 | OpenID protocol allows you to use an openid auth request. You can read more information about it here http://openid.net/ 1314 | 1315 | ```javascript 1316 | var everyauth = require('everyauth') 1317 | , connect = require('connect'); 1318 | 1319 | everyauth.openid 1320 | .myHostname('http://localhost:3000') 1321 | .simpleRegistration({ 1322 | "nickname" : true 1323 | , "email" : true 1324 | , "fullname" : true 1325 | , "dob" : true 1326 | , "gender" : true 1327 | , "postcode" : true 1328 | , "country" : true 1329 | , "language" : true 1330 | , "timezone" : true 1331 | }) 1332 | .attributeExchange({ 1333 | "http://axschema.org/contact/email" : "required" 1334 | , "http://axschema.org/namePerson/friendly" : "required" 1335 | , "http://axschema.org/namePerson" : "required" 1336 | , "http://axschema.org/namePerson/first" : "required" 1337 | , "http://axschema.org/contact/country/home": "required" 1338 | , "http://axschema.org/media/image/default" : "required" 1339 | , "http://axschema.org/x/media/signature" : "required" 1340 | }) 1341 | .openidURLField('openid_identifier'); //The POST variable used to get the OpenID 1342 | .findOrCreateUser( function(session, openIdUserAttributes) { 1343 | // find or create user logic goes here 1344 | }) 1345 | .redirectPath('/'); 1346 | 1347 | var routes = function (app) { 1348 | // Define your routes here 1349 | }; 1350 | 1351 | connect( 1352 | connect.bodyParser() 1353 | , connect.cookieParser() 1354 | , connect.session({secret: 'whodunnit'}) 1355 | , everyauth.middleware() 1356 | , connect.router(routes); 1357 | ).listen(3000); 1358 | ``` 1359 | 1360 | ## Setting up Google OpenID+OAuth Hybrid protocol 1361 | 1362 | OpenID+OAuth Hybrid protocol allows you to combine an openid auth request with a oauth access request. You can read more information about it here http://code.google.com/apis/accounts/docs/OpenID.html 1363 | 1364 | ```javascript 1365 | var everyauth = require('everyauth') 1366 | , connect = require('connect'); 1367 | 1368 | everyauth.googlehybrid 1369 | .consumerKey('YOUR CONSUMER ID HERE') 1370 | .consumerSecret('YOUR CONSUMER SECRET HERE') 1371 | .scope(['GOOGLE API SCOPE','GOOGLE API SCOPE']) 1372 | .findOrCreateUser( function(session, userAttributes) { 1373 | // find or create user logic goes here 1374 | }) 1375 | .redirectPath('/'); 1376 | 1377 | var routes = function (app) { 1378 | // Define your routes here 1379 | }; 1380 | 1381 | connect( 1382 | connect.bodyParser() 1383 | , connect.cookieParser() 1384 | , connect.session({secret: 'whodunnit'}) 1385 | , everyauth.middleware() 1386 | , connect.router(routes); 1387 | ).listen(3000); 1388 | ``` 1389 | 1390 | ## Setting up Box.net Auth 1391 | 1392 | ```javascript 1393 | var everyauth = require('everyauth') 1394 | , connect = require('connect'); 1395 | 1396 | everyauth.box 1397 | .apiKey('YOUR API KEY') 1398 | .findOrCreateUser( function (sess, authToken, boxUser) { 1399 | // find or create user logic goes here 1400 | // 1401 | // e.g., 1402 | // return usersByBoxId[user.user_id] || (usersByBoxId[user.user_id] = user); 1403 | }) 1404 | .redirectPath('/'); 1405 | 1406 | var routes = function (app) { 1407 | // Define your routes here 1408 | }; 1409 | 1410 | connect( 1411 | connect.bodyParser() 1412 | , connect.cookieParser() 1413 | , connect.session({secret: 'whodunnit'}) 1414 | , everyauth.middleware() 1415 | , connect.router(routes); 1416 | ).listen(3000); 1417 | ``` 1418 | 1419 | You can also configure more parameters (most are set to defaults) via 1420 | the same chainable API: 1421 | 1422 | ```javascript 1423 | everyauth.box 1424 | .entryPath('/auth/box') 1425 | .callbackPath('/auth/box/callback'); 1426 | ``` 1427 | 1428 | If you want to see what the current value of a 1429 | configured parameter is, you can do so via: 1430 | 1431 | ```javascript 1432 | everyauth.box.callbackPath(); // '/auth/box/callback' 1433 | everyauth.box.entryPath(); // '/auth/box' 1434 | ``` 1435 | 1436 | To see all parameters that are configurable, the following will return an 1437 | object whose parameter name keys map to description values: 1438 | 1439 | ```javascript 1440 | everyauth.box.configurable(); 1441 | ``` 1442 | 1443 | ## Setting up LDAP 1444 | 1445 | The LDAP module is still in development. Do not use it in production yet. 1446 | 1447 | Install OpenLDAP client libraries: 1448 | 1449 | $ apt-get install slapd ldap-utils 1450 | 1451 | Install [node-ldapauth](https://github.com/joewalnes/node-ldapauth): 1452 | 1453 | ```javascript 1454 | var everyauth = require('everyauth') 1455 | , connect = require('connect'); 1456 | 1457 | everyauth.ldap 1458 | .host('your.ldap.host') 1459 | .port(389) 1460 | 1461 | // The `ldap` module inherits from the `password` module, so 1462 | // refer to the `password` module instructions several sections above 1463 | // in this README. 1464 | // You do not need to configure the `authenticate` step as instructed 1465 | // by `password` because the `ldap` module already does that for you. 1466 | // Moreover, all the registration related steps and configurable parameters 1467 | // are no longer valid 1468 | .getLoginPath(...) 1469 | .postLoginPath(...) 1470 | .loginView(...) 1471 | .loginSuccessRedirect(...); 1472 | 1473 | var routes = function (app) { 1474 | // Define your routes here 1475 | }; 1476 | 1477 | connect( 1478 | connect.bodyParser() 1479 | , connect.cookieParser() 1480 | , connect.session({secret: 'whodunnit'}) 1481 | , everyauth.middleware() 1482 | , connect.router(routes); 1483 | ).listen(3000); 1484 | ``` 1485 | 1486 | ## Accessing the User 1487 | 1488 | If you are using `express` or `connect`, then `everyauth` 1489 | provides an easy way to access the user as: 1490 | 1491 | - `req.user` from your app server 1492 | - `everyauth.user` via the `everyauth` helper accessible from your `express` views. 1493 | - `user` as a helper accessible from your `express` views 1494 | 1495 | To access the user, configure `everyauth.everymodule.findUserById`. 1496 | For example, using [mongoose](http://github.com/LearnBoost/mongoose): 1497 | 1498 | ```javascript 1499 | everyauth.everymodule.findUserById( function (userId, callback) { 1500 | User.findById(userId, callback); 1501 | // callback has the signature, function (err, user) {...} 1502 | }); 1503 | ``` 1504 | 1505 | Once you have configured this method, you now have access to the user object 1506 | that was fetched anywhere in your server app code as `req.user`. For instance: 1507 | 1508 | ```javascript 1509 | var app = require('express').createServer() 1510 | 1511 | // Configure your app 1512 | 1513 | app.get('/', function (req, res) { 1514 | console.log(req.user); // FTW! 1515 | res.render('home'); 1516 | }); 1517 | ``` 1518 | 1519 | Moreover, you can access the user in your views as `everyauth.user` or as `user`. 1520 | 1521 | //- Inside ./views/home.jade 1522 | span.user-id= everyauth.user.name 1523 | #user-id= user.id 1524 | 1525 | ## Express Helpers 1526 | 1527 | If you are using express, everyauth comes with some useful dynamic helpers. 1528 | To enable them: 1529 | 1530 | ```javascript 1531 | var express = require('express') 1532 | , everyauth = require('everyauth') 1533 | , app = express.createServer(); 1534 | 1535 | everyauth.helpExpress(app); 1536 | ``` 1537 | 1538 | Then, from within your views, you will have access to the following helpers methods 1539 | attached to the helper, `everyauth`: 1540 | 1541 | - `everyauth.loggedIn` 1542 | - `everyauth.user` - the User document associated with the session 1543 | - `everyauth.facebook` - The is equivalent to what is stored at `req.session.auth.facebook`, 1544 | so you can do things like ... 1545 | - `everyauth.facebook.user` - returns the user json provided from the OAuth provider. 1546 | - `everyauth.facebook.accessToken` - returns the access_token provided from the OAuth provider 1547 | for authorized API calls on behalf of the user. 1548 | - And you also get this pattern for other modules - e.g., `everyauth.twitter.user`, 1549 | `everyauth.github.user`, etc. 1550 | 1551 | You also get access to the view helper 1552 | 1553 | - `user` - the same as `everyauth.user` above 1554 | 1555 | As an example of how you would use these, consider the following `./views/user.jade` jade template: 1556 | 1557 | .user-id 1558 | .label User Id 1559 | .value #{user.id} 1560 | .facebook-id 1561 | .label User Facebook Id 1562 | .value #{everyauth.facebook.user.id} 1563 | 1564 | `everyauth` also provides convenience methods on the `ServerRequest` instance `req`. 1565 | From any scope that has access to `req`, you get the following convenience getters and methods: 1566 | 1567 | - `req.loggedIn` - a Boolean getter that tells you if the request is by a logged in user 1568 | - `req.user` - the User document associated with the session 1569 | - `req.logout()` - clears the sesion of your auth data 1570 | 1571 | ## Configuring a Module 1572 | 1573 | everyauth was built with powerful configuration needs in mind. 1574 | 1575 | Every module comes with a set of parameters that you can configure 1576 | directly. To see a list of those parameters on a per module basis, 1577 | with descriptions about what they do, enter the following into the 1578 | node REPL (to access the REPL, just type `node` at the command line) 1579 | 1580 | > var ea = require('everyauth'); 1581 | > ea.facebook.configurable(); 1582 | 1583 | For example, you will see that one of the configuration parameters is 1584 | `moduleTimeout`, which is described to be `how long to wait per step 1585 | before timing out and invoking any timeout callbacks` 1586 | 1587 | Every configuration parameter corresponds to a method of the same name 1588 | on the auth module under consideration (i.e., in this case 1589 | `ea.facebook`). To create or over-write that parameter, just 1590 | call that method with the new value as the argument: 1591 | 1592 | ```javascript 1593 | ea.facebook 1594 | .moduleTimeout( 4000 ); // Wait 4 seconds before timing out any step 1595 | // involved in the facebook auth process 1596 | ``` 1597 | 1598 | Configuration parameters can be scalars. But they can be anything. For 1599 | example, they can also be functions, too. The facebook module has a 1600 | configurable step named `findOrCreateUser` that is described as 1601 | "STEP FN [findOrCreateUser] function encapsulating the logic for the step 1602 | `fetchOAuthUser`.". What this means is that this configures the 1603 | function (i.e., "FN") that encapsulates the logic of this step. 1604 | 1605 | ```javascript 1606 | ea.facebook 1607 | .findOrCreateUser( function (session, accessToken, extra, oauthUser) { 1608 | // find or create user logic goes here 1609 | }); 1610 | ``` 1611 | 1612 | How do we know what arguments the function takes? 1613 | We elaborate more about step function configuration in our 1614 | `Introspection` section below. 1615 | 1616 | ## Introspection 1617 | 1618 | everyauth provides convenient methods and getters for finding out 1619 | about any module. 1620 | 1621 | Show all configurable parameters with their descriptions: 1622 | 1623 | ```javascript 1624 | everyauth.facebook.configurable(); 1625 | ``` 1626 | 1627 | Show the value of a single configurable parameter: 1628 | 1629 | ```javascript 1630 | // Get the value of the configurable callbackPath parameter 1631 | everyauth.facebook.callbackPath(); // => '/auth/facebook/callback' 1632 | ``` 1633 | 1634 | Show the declared routes (pretty printed): 1635 | 1636 | ```javascript 1637 | everyauth.facebook.routes; 1638 | ``` 1639 | 1640 | Show the steps initiated by a given route: 1641 | 1642 | ```javascript 1643 | everyauth.facebook.route.get.entryPath.steps; 1644 | everyauth.facebook.route.get.callbackPath.steps; 1645 | ``` 1646 | 1647 | Sometimes you need to set up additional steps for a given auth 1648 | module, by defining that step in your app. For example, the 1649 | set of steps triggered when someone requests the facebook 1650 | module's `callbackPath` contains a step that you must define 1651 | in your app. To see what that step is, you can introspect 1652 | the `callbackPath` route with the facebook module. 1653 | 1654 | ```javascript 1655 | everyauth.facebook.route.get.callbackPath.steps.incomplete; 1656 | // => [ { name: 'findOrCreateUser', 1657 | // error: 'is missing: its function' } ] 1658 | ``` 1659 | 1660 | This tells you that you must define the function that defines the 1661 | logic for the `findOrCreateUser` step. To see what the function 1662 | signature looks like for this step: 1663 | 1664 | ```javascript 1665 | var matchingStep = 1666 | everyauth.facebook.route.get.callbackPath.steps.filter( function (step) { 1667 | return step.name === 'findOrCreateUser'; 1668 | })[0]; 1669 | // { name: 'findOrCreateUser', 1670 | // accepts: [ 'session', 'accessToken', 'extra', 'oauthUser' ], 1671 | // promises: [ 'user' ] } 1672 | ``` 1673 | 1674 | This tells you that the function should take the following 4 arguments: 1675 | 1676 | ```javascript 1677 | function (session, accessToken, extra, oauthUser) { 1678 | ... 1679 | } 1680 | ``` 1681 | 1682 | And that the function should return a `user` that is a user object or 1683 | a Promise that promises a user object. 1684 | 1685 | ```javascript 1686 | // For synchronous lookup situations, you can return a user 1687 | function (session, accessToken, extra, oauthUser) { 1688 | ... 1689 | return { id: 'some user id', username: 'some user name' }; 1690 | } 1691 | 1692 | // OR 1693 | 1694 | // For asynchronous lookup situations, you must return a Promise that 1695 | // will be fulfilled with a user later on 1696 | function (session, accessToken, extra, oauthUser) { 1697 | var promise = this.Promise(); 1698 | asyncFindUser( function (err, user) { 1699 | if (err) return promise.fail(err); 1700 | promise.fulfill(user); 1701 | }); 1702 | return promise; 1703 | } 1704 | ``` 1705 | 1706 | You add this function as the block for the step `findOrCreateUser` just like 1707 | you configure any other configurable parameter in your auth module: 1708 | 1709 | ```javascript 1710 | everyauth.facebook 1711 | .findOrCreateUser( function (session, accessToken, extra, oauthUser) { 1712 | // Logic goes here 1713 | }); 1714 | ``` 1715 | 1716 | There are also several other introspection tools at your disposal: 1717 | 1718 | For example, to show the submodules of an auth module by name: 1719 | 1720 | ```javascript 1721 | everyauth.oauth2.submodules; 1722 | ``` 1723 | 1724 | Other introspection tools to describe (explanations coming soon): 1725 | 1726 | - *Invalid Steps* 1727 | 1728 | ```javascript 1729 | everyauth.facebook.routes.get.callbackPath.steps.invalid 1730 | ``` 1731 | 1732 | ## Debugging 1733 | 1734 | ### Debugging - Logging Module Steps 1735 | 1736 | To turn on debugging: 1737 | 1738 | ```javascript 1739 | everyauth.debug = true; 1740 | ``` 1741 | 1742 | Each everyauth auth strategy module is composed of steps. As each step begins and ends, everyauth will print out to the console the beginning and end of each step. So by turning on the debug flag, you get insight into what step everyauth is executing at any time. 1743 | 1744 | ### Debugging - Configuring Error Handling 1745 | 1746 | By default, all modules handle errors by throwing them. That said, `everyauth` allows 1747 | you to over-ride this behavior. 1748 | 1749 | You can configure error handling at the module and step level. To handle *all* 1750 | errors in the same manner across all auth modules that you use, do the following. 1751 | 1752 | ```javascript 1753 | everyauth.everymodule.moduleErrback( function (err) { 1754 | // Do something with the err -- e.g., log it, throw it 1755 | }); 1756 | ``` 1757 | 1758 | You can also configure your error handling on a per module basis. So, for example, if 1759 | you want to handle errors during the Facebook module differently than in other modules: 1760 | 1761 | 1762 | ```javascript 1763 | everyauth.facebook.moduleErrback( function (err) { 1764 | // Do something with the err -- e.g., log it, throw it 1765 | }); 1766 | ``` 1767 | 1768 | ### Debugging - Setting Timeouts 1769 | 1770 | By default, every module has 10 seconds to complete each step. If a step takes longer than 10 seconds to complete, then everyauth will pass a timeout error to your configured error handler (see section "Configure Error Handling" above). 1771 | 1772 | If you would like to increase or decrease the timeout period across all modules, you can do so via: 1773 | 1774 | ```javascript 1775 | everyauth.everymodule.moduleTimeout(2000); // Wait 2 seconds per step instead before timing out 1776 | ``` 1777 | 1778 | You can eliminate the timeout altogether by configuring your timeouts to -1: 1779 | 1780 | ```javascript 1781 | everyauth.everymodule.moduleTimeout(-1); 1782 | ``` 1783 | 1784 | You can also configure the timeout period on a per module basis. For example, the following will result in the facebook module having 3 seconds to complete each step before timing out; all other modules will have the default 10 seconds per step before timing out. 1785 | 1786 | ```javascript 1787 | everyauth.facebook.moduleTimeout(3000); // Wait 3 seconds 1788 | ``` 1789 | 1790 | ## Modules and Projects that use everyauth 1791 | 1792 | Currently, the following module uses everyauth. If you are using everyauth 1793 | in a project, app, or module, get in touch to get added to the list below: 1794 | 1795 | - [mongoose-auth](https://github.com/bnoguchi/mongoose-auth) Authorization plugin 1796 | for use with the node.js MongoDB orm. 1797 | 1798 | --- 1799 | ### Author 1800 | Brian Noguchi 1801 | 1802 | ### Credits 1803 | 1804 | Thanks to the following contributors for the following modules: 1805 | 1806 | - [RocketLabs Development](https://github.com/rocketlabsdev) for contributing 1807 | - OpenId 1808 | - Google Hybrid 1809 | - [Andrew Mee](https://github.com/starfishmod) 1810 | - OpenId 1811 | - [Alfred Nerstu](https://github.com/alfrednerstu) 1812 | - Readability 1813 | - [Torgeir](https://github.com/torgeir) 1814 | - DropBox 1815 | - [slickplaid](https://github.com/slickplaid) 1816 | - Justin.tv 1817 | - Vimeo 1818 | - [Andrew Kramolisch](https://github.com/andykram) 1819 | - Gowalla 1820 | 1821 | ### MIT License 1822 | Copyright (c) 2011 by Brian Noguchi 1823 | 1824 | Permission is hereby granted, free of charge, to any person obtaining a copy 1825 | of this software and associated documentation files (the "Software"), to deal 1826 | in the Software without restriction, including without limitation the rights 1827 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1828 | copies of the Software, and to permit persons to whom the Software is 1829 | furnished to do so, subject to the following conditions: 1830 | 1831 | The above copyright notice and this permission notice shall be included in 1832 | all copies or substantial portions of the Software. 1833 | 1834 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1835 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1836 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1837 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1838 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1839 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 1840 | THE SOFTWARE. 1841 | --------------------------------------------------------------------------------