├── lib └── modules │ ├── everymodule │ ├── schema.js │ ├── plugin.js │ ├── index.js │ └── everyauth.js │ ├── password │ ├── schema.js │ ├── index.js │ ├── everyauth.js │ └── plugin.js │ ├── google │ ├── index.js │ ├── schema.js │ ├── everyauth.js │ └── plugin.js │ ├── facebook │ ├── index.js │ ├── schema.js │ ├── everyauth.js │ └── plugin.js │ ├── github │ ├── index.js │ ├── everyauth.js │ ├── schema.js │ └── plugin.js │ ├── instagram │ ├── index.js │ ├── schema.js │ ├── everyauth.js │ └── plugin.js │ ├── twitter │ ├── index.js │ ├── everyauth.js │ ├── schema.js │ └── plugin.js │ ├── confirmable │ ├── view.jade │ └── connect.js │ └── registration │ └── connect.js ├── .gitignore ├── Makefile ├── example ├── views │ ├── layout.jade │ ├── login.jade │ ├── register.jade │ └── home.jade ├── conf.js └── server.js ├── package.json ├── test └── authplugin.test.js ├── index.js └── README.md /lib/modules/everymodule/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.swp 2 | node_modules 3 | issues/ 4 | .project 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @node_modules/.bin/mocha 3 | 4 | .PHONY: test 5 | -------------------------------------------------------------------------------- /lib/modules/password/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | login: { type: String, unique: true } 3 | , salt: String 4 | , hash: String 5 | }; 6 | -------------------------------------------------------------------------------- /lib/modules/everymodule/plugin.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | module.exports = function everymodule (schema, opts) { 4 | return; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/modules/google/index.js: -------------------------------------------------------------------------------- 1 | exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); -------------------------------------------------------------------------------- /lib/modules/facebook/index.js: -------------------------------------------------------------------------------- 1 | exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); 4 | -------------------------------------------------------------------------------- /lib/modules/github/index.js: -------------------------------------------------------------------------------- 1 | exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); 4 | -------------------------------------------------------------------------------- /lib/modules/instagram/index.js: -------------------------------------------------------------------------------- 1 | exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); 4 | -------------------------------------------------------------------------------- /lib/modules/twitter/index.js: -------------------------------------------------------------------------------- 1 | exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); 4 | -------------------------------------------------------------------------------- /lib/modules/everymodule/index.js: -------------------------------------------------------------------------------- 1 | exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); 4 | -------------------------------------------------------------------------------- /lib/modules/google/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | google:{ 3 | accessToken: String 4 | , expires: Date 5 | , refreshToken: String 6 | , email: String 7 | } 8 | } -------------------------------------------------------------------------------- /example/views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | title mongoose-auth example 5 | script(src='http://static.ak.fbcdn.net/connect/en_US/core.js') 6 | body 7 | h1 mongoose-auth Example 8 | #main 9 | != body 10 | -------------------------------------------------------------------------------- /lib/modules/confirmable/view.jade: -------------------------------------------------------------------------------- 1 | h2 Resend Confirmation Instructions 2 | 3 | form(action: 'POST', '/confirmation') 4 | label 5 | input(type: 'text', name: 'email') 6 | input(type: 'submit', value: 'Resend confirmation instructions') 7 | -------------------------------------------------------------------------------- /lib/modules/everymodule/everyauth.js: -------------------------------------------------------------------------------- 1 | var Promise = require('everyauth').Promise; 2 | 3 | // Defaults 4 | 5 | module.exports = { 6 | findUserById: function (userId, fn) { 7 | this.User()().findById(userId, fn); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /lib/modules/instagram/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | instagram: { 3 | id: String 4 | , username: String 5 | , name: { 6 | first: String 7 | , last: String 8 | } 9 | , profilePicture: String 10 | , counts: { 11 | media: Number 12 | , follows: Number 13 | , followedBy: Number 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/modules/registration/connect.js: -------------------------------------------------------------------------------- 1 | module.exports = function routes (app) { 2 | app.post('/users', function (req, res) { 3 | var params = req.param.body; 4 | User.create(params, function (err, user) { 5 | if (err instanceof mongoose.ValidatorError) { 6 | res.render('new.jade'); 7 | } else { 8 | res.redirect('/'); 9 | } 10 | }); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /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/modules/facebook/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fb: { 3 | id: String 4 | , accessToken: String 5 | , expires: Date 6 | , name: { 7 | full: String 8 | , first: String 9 | , last: String 10 | } 11 | , fbAlias: String 12 | , gender: String 13 | , email: String 14 | // , email: Email // TODO Try to add Email type back in 15 | // Broken because of require behavior 16 | , timezone: String 17 | , locale: String 18 | , verified: Boolean 19 | , updatedTime: String 20 | , phone: String 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/modules/github/everyauth.js: -------------------------------------------------------------------------------- 1 | // Defaults 2 | module.exports = { 3 | findOrCreateUser: function (sess, accessTok, accessTokExtra, ghUser) { 4 | var promise = this.Promise() 5 | , self = this; 6 | // TODO Check user in session or request helper first 7 | // e.g., req.user or sess.auth.userId 8 | this.User()().findOne({'github.id': ghUser.id}, function (err, foundUser) { 9 | if (foundUser) 10 | return promise.fulfill(foundUser); 11 | self.User()().createWithGithub(ghUser, accessTok, function (err, createdUser) { 12 | return promise.fulfill(createdUser); 13 | }); 14 | }); 15 | return promise; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/modules/instagram/everyauth.js: -------------------------------------------------------------------------------- 1 | // Defaults 2 | module.exports = { 3 | findOrCreateUser: function (sess, accessTok, accessTokExtra, hipster) { 4 | var promise = this.Promise() 5 | , self = this; 6 | // TODO Check user in session or request helper first 7 | // e.g., req.user or sess.auth.userId 8 | this.User()().findOne({'instagram.id': hipster.id}, function (err, foundUser) { 9 | if (foundUser) 10 | return promise.fulfill(foundUser); 11 | self.User()().createWithInstagram(hipster, accessTok, function (err, createdUser) { 12 | return promise.fulfill(createdUser); 13 | }); 14 | }); 15 | return promise; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/modules/twitter/everyauth.js: -------------------------------------------------------------------------------- 1 | // Defaults 2 | module.exports = { 3 | findOrCreateUser: function (sess, accessTok, accessTokSecret, twitterUser) { 4 | var promise = this.Promise() 5 | , self = this; 6 | this.User()().findOne({'twit.id': twitterUser.id}, function (err, foundUser) { 7 | if (err) return promise.fail(err); 8 | if (foundUser) { 9 | return promise.fulfill(foundUser); 10 | } 11 | self.User()().createWithTwitter(twitterUser, accessTok, accessTokSecret, function (err, createdUser) { 12 | if (err) return promise.fail(err); 13 | return promise.fulfill(createdUser); 14 | }); 15 | }); 16 | return promise; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/modules/confirmable/connect.js: -------------------------------------------------------------------------------- 1 | module.exports = function routes (app) { 2 | app.get('/confirmation/new', function (req, res) { 3 | res.render('./view.jade'); 4 | }); 5 | 6 | app.post('/confirmation', function (req, res) { 7 | // TODO Send confirmation instructions via email 8 | }); 9 | 10 | app.get('/confirmation/:confirmation_token', function (req, res) { 11 | var token = req.param('confirmation_token'); 12 | // TODO import User 13 | User.findOne({confirmation_token: token}, function (err, user) { 14 | if (user.isPersisted()) 15 | user.confirm(); 16 | // TODO sign in 17 | // TODO Set flash messages 18 | res.redirect('/'); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/modules/facebook/everyauth.js: -------------------------------------------------------------------------------- 1 | // Defaults 2 | module.exports = { 3 | findOrCreateUser: function (sess, accessTok, accessTokExtra, fbUser) { 4 | var promise = this.Promise() 5 | , User = this.User()(); 6 | // TODO Check user in session or request helper first 7 | // e.g., req.user or sess.auth.userId 8 | User.findOne({'fb.id': fbUser.id}, function (err, foundUser) { 9 | if (foundUser) { 10 | return promise.fulfill(foundUser); 11 | } 12 | console.log("CREATING"); 13 | User.createWithFB(fbUser, accessTok, accessTokExtra.expires, function (err, createdUser) { 14 | if (err) return promise.fail(err); 15 | return promise.fulfill(createdUser); 16 | }); 17 | }); 18 | return promise; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/modules/google/everyauth.js: -------------------------------------------------------------------------------- 1 | // Defaults 2 | module.exports = { 3 | findOrCreateUser: function (sess, accessTok, accessTokExtra, googleUser) { 4 | var promise = this.Promise() 5 | , User = this.User()(); 6 | // TODO Check user in session or request helper first 7 | // e.g., req.user or sess.auth.userId 8 | User.findOne({'google.email': googleUser.id}, function (err, foundUser) { 9 | if (foundUser) { 10 | return promise.fulfill(foundUser); 11 | } 12 | console.log("CREATING"); 13 | User.createWithGoogleOAuth(googleUser, accessTok, accessTokExtra, function (err, createdUser) { 14 | if (err) return promise.fail(err); 15 | return promise.fulfill(createdUser); 16 | }); 17 | }); 18 | return promise; 19 | } 20 | }; -------------------------------------------------------------------------------- /lib/modules/github/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | github: { 3 | id: Number 4 | , login: String 5 | , gravatarId: String 6 | , name: String 7 | , email: String 8 | , publicRepoCount: Number 9 | , publicGistCount: Number 10 | , followingCount: Number 11 | , followersCount: Number 12 | , company: String 13 | , blog: String 14 | , location: String 15 | , permission: String 16 | , createdAt: Date 17 | 18 | // Private data 19 | , totalPrivateRepoCount: Number 20 | , collaborators: Number 21 | , diskUsage: Number 22 | , ownedPrivateRepoCount: Number 23 | , privateGistCount: Number 24 | , plan: { 25 | name: String 26 | , collaborators: Number 27 | , space: Number 28 | , privateRepos: Number 29 | } 30 | } 31 | , 'github.type': String 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongoose-auth", 3 | "description": "User authentication plugin for mongoose node.js orm", 4 | "version": "0.0.12", 5 | "homepage": "https://github.com/bnoguchi/mongoose-auth/", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/bnoguchi/mongoose-auth.git" 9 | }, 10 | "author": "Brian Noguchi (https://github.com/bnoguchi/)", 11 | "main": "./index.js", 12 | "directories": { 13 | "lib": "." 14 | }, 15 | "dependencies": { 16 | "bcrypt": ">=0.5.0", 17 | "mongoose": ">=2.4.8", 18 | "everyauth": ">=0.2.28", 19 | "mongoose-types": ">=1.0.3" 20 | }, 21 | "devDependencies": { 22 | "express": ">=2.3.2", 23 | "jade": ">=0.12.1", 24 | "mocha": ">=0.10.1", 25 | "should": ">=0.5.1" 26 | }, 27 | "scripts": { 28 | "test": "make" 29 | }, 30 | "engines": { 31 | "node": ">=0.4.0" 32 | } 33 | } -------------------------------------------------------------------------------- /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 | #phone 14 | label(for='phone') Phone 15 | input(type='text', name='phone') 16 | #first-name 17 | label(for='name.first') First Name 18 | input(type='text', name='name.first') 19 | #last-name 20 | label(for='name.last') Last Name 21 | input(type='text', name='name.last') 22 | #submit 23 | input(type='submit') Register 24 | -------------------------------------------------------------------------------- /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 | , google: { 23 | clientId: '224794776836-cp3a2v0elt955h9uqhgmskplhg85ljjm.apps.googleusercontent.com' 24 | , clientSecret: 'rxGFo1mBG_H3DX2ifDFawiMZ' 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/modules/google/plugin.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | , mongooseTypes = require('mongoose-types') 3 | , _schema = require('./schema') 4 | , everyauth = require('everyauth'); 5 | mongooseTypes.loadTypes(mongoose); 6 | 7 | module.exports = function google (schema, opts) { 8 | schema.add(_schema); 9 | 10 | schema.static('createWithGoogleOAuth', function (googleUser, accessToken, accessTokenExtra, callback) { 11 | var expiresDate = new Date; 12 | expiresDate.setSeconds(expiresDate.getSeconds() + accessTokenExtra.expires_in); 13 | 14 | var params = { 15 | google: { 16 | email: googleUser.id 17 | , expires: expiresDate 18 | , accessToken: accessToken 19 | , refreshToken: accessTokenExtra.refresh_token 20 | } 21 | }; 22 | 23 | // TODO Only do this if password module is enabled 24 | // Currently, this is not a valid way to check for enabled 25 | if (everyauth.password) 26 | params[everyauth.password.loginKey()] = "google:" + googleUser.id; // Hack because of way mongodb treate unique indexes 27 | 28 | this.create(params, callback); 29 | }); 30 | }; -------------------------------------------------------------------------------- /lib/modules/twitter/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | twit: { 3 | accessToken: String 4 | , accessTokenSecret: String 5 | , id: String 6 | , name: String 7 | , screenName: String 8 | , location: String 9 | , description: String 10 | , profileImageUrl: String 11 | , url: String // TODO Convert to URL from mongoose-types 12 | , 'protected': Boolean 13 | , followersCount: Number 14 | , profileBackgroundColor: String 15 | , profileTextColor: String 16 | , profileLinkColor: String 17 | , profileSidebarFillColor: String 18 | , profileSidebarBorderColor: String 19 | , friendsCount: Number 20 | , createdAt: Date 21 | , favouritesCount: Number 22 | , utcOffset: Number 23 | , timeZone: String 24 | , profileBackgroundImageUrl: String 25 | , profileBackgroundTile: Boolean 26 | , profileUseBackgroundImage: Boolean 27 | // , notifications: Boolean 28 | , geoEnabled: Boolean 29 | , verified: Boolean 30 | // , following: Boolean 31 | , statusesCount: Number 32 | , lang: String 33 | , contributorsEnabled: Boolean 34 | // , status: StatusSchema // only if public or you follow them + protected 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/modules/instagram/plugin.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | , mongooseTypes = require('mongoose-types') 3 | , _schema = require('./schema') 4 | , everyauth = require('everyauth'); 5 | mongooseTypes.loadTypes(mongoose); 6 | var Email = mongoose.SchemaTypes.Email; 7 | 8 | module.exports = function instagram (schema, opts) { 9 | schema.add(_schema); 10 | 11 | schema.static('createWithInstagram', function (hipster, accessToken, callback) { 12 | var params = { 13 | instagram: { 14 | id: hipster.id 15 | , name: { 16 | first: hipster.first_name 17 | , last: hipster.last_name 18 | } 19 | , profilePicture: hipster.profile_picture 20 | , counts: { 21 | media: hipster.counts.media 22 | , follows: hipster.counts.follows 23 | , followedBy: hipster.counts.followed_by 24 | } 25 | } 26 | }; 27 | 28 | // TODO Only do this if password module is enabled 29 | // Currently, this is not a valid way to check for enabled 30 | if (everyauth.password) 31 | params[everyauth.password.loginKey()] = "instagram:" + hipster.id; // Hack because of way mongodb treate unique indexes 32 | 33 | this.create(params, callback); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /test/authplugin.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | , mongoose = require('mongoose') 3 | , UserSchema = new mongoose.Schema() 4 | , authPlugin = require('../index') 5 | , User; 6 | 7 | UserSchema.plugin(authPlugin, { 8 | password: true 9 | }); 10 | 11 | mongoose.model('User', UserSchema); 12 | User = mongoose.model('User'); 13 | 14 | describe('User', function () { 15 | it('should generate a salt and set a hash when password is set', function () { 16 | var user = new User(); 17 | should.strictEqual(undefined, user.salt); 18 | should.strictEqual(undefined, user.hash); 19 | user.password = 'hello'; 20 | user.password.should.equal('hello'); 21 | user.salt.should.not.be.undefined; 22 | user.hash.should.not.be.undefined; 23 | }); 24 | it('should authenticate with a correct password', function (done) { 25 | var user = new User(); 26 | user.password = 'hello'; 27 | user.authenticate('hello', function (err, matched) { 28 | matched.should.be.true; 29 | done(); 30 | }); 31 | }); 32 | it('should fail authentication with an incorrect password', function (done) { 33 | var user = new User(); 34 | user.password = 'correct'; 35 | user.authenticate('incorrect', function (err, matched) { 36 | matched.should.be.false; 37 | done(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /lib/modules/facebook/plugin.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | , mongooseTypes = require('mongoose-types') 3 | , _schema = require('./schema') 4 | , everyauth = require('everyauth'); 5 | mongooseTypes.loadTypes(mongoose); 6 | var Email = mongoose.SchemaTypes.Email; 7 | 8 | module.exports = function facebook (schema, opts) { 9 | schema.add(_schema); 10 | 11 | schema.static('createWithFB', function (fbUserMeta, accessToken, expires, callback) { 12 | var expiresDate = new Date; 13 | expiresDate.setSeconds(expiresDate.getSeconds() + expires); 14 | 15 | var params = { 16 | fb: { 17 | id: fbUserMeta.id 18 | , accessToken: accessToken 19 | , expires: expiresDate 20 | , name: { 21 | full: fbUserMeta.name 22 | , first: fbUserMeta.first_name 23 | , last: fbUserMeta.last_name 24 | } 25 | , alias: fbUserMeta.link.match(/^http:\/\/www.facebook\.com\/(.+)/)[1] 26 | , gender: fbUserMeta.gender 27 | , email: fbUserMeta.email 28 | , timezone: fbUserMeta.timezone 29 | , locale: fbUserMeta.locale 30 | , verified: fbUserMeta.verified 31 | , updatedTime: fbUserMeta.updated_time 32 | } 33 | }; 34 | 35 | // TODO Only do this if password module is enabled 36 | // Currently, this is not a valid way to check for enabled 37 | if (everyauth.password) 38 | params[everyauth.password.loginKey()] = "fb:" + fbUserMeta.id; // Hack because of way mongodb treate unique indexes 39 | 40 | this.create(params, callback); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/modules/password/index.js: -------------------------------------------------------------------------------- 1 | var schema = exports.schema = require('./schema'); 2 | exports.plugin = require('./plugin'); 3 | exports.everyauth = require('./everyauth'); 4 | var everyauth = require('everyauth'); 5 | //exports.router = require('./connect').router; 6 | 7 | Object.defineProperty(exports, 'specialOptHandlers', { 8 | value: { 9 | loginWith: function (value) { 10 | if (value) { 11 | delete schema.login; 12 | schema[value] = { type: String, unique: true }; 13 | everyauth.password.loginWith(value); 14 | } 15 | } 16 | , extraParams: function (value) { 17 | if (value && value.constructor == Object) { 18 | for (var k in value) { 19 | schema[k] = value[k]; 20 | } 21 | everyauth.password.extractExtraRegistrationParams( function (req) { 22 | function recurse (obj, ret, prefix) { 23 | ret || (ret = {}); 24 | prefix || (prefix = []); 25 | return Object.keys(obj).reduce( function (ret, k) { 26 | prefix.push(k); 27 | if (obj[k].constructor == Object && !obj[k].type) { 28 | ret[k] = recurse(obj[k], {}, prefix); 29 | } else { 30 | ret[k] = req.body[prefix.join('.')]; 31 | } 32 | prefix.pop(k); 33 | return ret; 34 | }, ret); 35 | } 36 | var params = recurse(value); 37 | return params; 38 | }); 39 | } 40 | } 41 | } 42 | , enumerable: false 43 | }); 44 | -------------------------------------------------------------------------------- /lib/modules/password/everyauth.js: -------------------------------------------------------------------------------- 1 | // Defaults 2 | module.exports = { 3 | authenticate: function (login, password) { 4 | var promise 5 | , errors = []; 6 | if (!login) errors.push('Missing login.'); 7 | if (!password) errors.push('Missing password.'); 8 | if (errors.length) return errors; 9 | 10 | promise = this.Promise(); 11 | this.User()().authenticate(login, password, function (err, user) { 12 | if (err) { 13 | errors.push(err.message || err); 14 | return promise.fulfill(errors); 15 | } 16 | if (!user) { 17 | errors.push('Failed login.'); 18 | return promise.fulfill(errors); 19 | } 20 | promise.fulfill(user); 21 | }); 22 | return promise; 23 | }, 24 | 25 | validateRegistration: function (newUserAttrs, errors) { 26 | var promise = this.Promise() 27 | , User = this.User()() 28 | , user = new User(newUserAttrs); 29 | user.validate( function (err) { 30 | if (err) { 31 | errors.push(err.message || err); 32 | } 33 | if (errors.length) 34 | return promise.fulfill(errors); 35 | promise.fulfill(null); 36 | }); 37 | return promise; 38 | }, 39 | 40 | registerUser: function (newUserAttrs) { 41 | var promise = this.Promise(); 42 | this.User()().create(newUserAttrs, function (err, createdUser) { 43 | if (err) { 44 | console.log(err); // TODO Make depend on debug flag 45 | if (/duplicate key/.test(err)) { 46 | return promise.fulfill(['Someone already has claimed that login.']); 47 | } 48 | return promise.fail(err); 49 | } 50 | promise.fulfill(createdUser); 51 | }); 52 | return promise; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /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(style='float: left; margin-left: 5px') 8 | a(href='/auth/facebook', style='border: 0px') 9 | img(style='border: 0px', src='http://github.com/intridea/authbuttons/raw/master/png/facebook_64.png') 10 | #twitter-login(style='float: left; margin-left: 5px') 11 | a(href='/auth/twitter', style='border: 0px') 12 | img(style='border: 0px', src='http://github.com/intridea/authbuttons/raw/master/png/twitter_64.png') 13 | #github-login(style='float: left; margin-left: 5px') 14 | a(href='/auth/github', style='border: 0px') 15 | img(style='border: 0px', src='http://github.com/intridea/authbuttons/raw/master/png/github_64.png') 16 | #google-login(style='float: left; margin-left: 5px') 17 | a(href='/auth/google', style='border: 0px') 18 | img(style='border: 0px', src='http://github.com/intridea/authbuttons/raw/master/png/google_64.png') 19 | #instagram-login(style='float: left; margin-left: 5px') 20 | a(href='/auth/instagram', style='border: 0px') 21 | img(style='border: 0px', src='https://instagram.com/static/images/headerWithTitle.png') 22 | - else 23 | h2 Authenticated 24 | #user-id Logged in with `user.id` #{user.id} - aka `everyauth.user.id` #{everyauth.user.id} 25 | - if (everyauth.facebook) 26 | h3 Facebook User Data 27 | p= JSON.stringify(everyauth.facebook.user) 28 | - if (everyauth.twitter) 29 | h3 Twitter User Data 30 | p= JSON.stringify(everyauth.twitter.user) 31 | - if (everyauth.github) 32 | h3 GitHub User Data 33 | p= JSON.stringify(everyauth.github.user) 34 | - if (everyauth.google) 35 | h3 Google User Data 36 | p= JSON.stringify(everyauth.google.user) 37 | - if (everyauth.instagram) 38 | h3 Instagram User Data 39 | p= JSON.stringify(everyauth.instagram.user) 40 | h3 41 | a(href='/logout') Logout 42 | -------------------------------------------------------------------------------- /lib/modules/github/plugin.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | , mongooseTypes = require('mongoose-types') 3 | , _schema = require('./schema') 4 | , everyauth = require('everyauth'); 5 | mongooseTypes.loadTypes(mongoose); 6 | var Email = mongoose.SchemaTypes.Email; 7 | 8 | module.exports = function github (schema, opts) { 9 | schema.add(_schema); 10 | 11 | schema.static('createWithGithub', function (ghUser, accessToken, callback) { 12 | var params = { 13 | github: { 14 | id: ghUser.id 15 | , type: ghUser.type 16 | , login: ghUser.login 17 | , gravatarId: ghUser.gravatar_id 18 | , name: ghUser.name 19 | , email: ghUser.email 20 | , publicRepoCount: ghUser.public_repo_count 21 | , publicGistCount: ghUser.public_gist_count 22 | , followingCount: ghUser.following_count 23 | , followersCount: ghUser.followers_count 24 | , company: ghUser.company 25 | , blog: ghUser.blog 26 | , location: ghUser.location 27 | , permission: ghUser.permission 28 | , createdAt: ghUser.created_at 29 | 30 | // Private data 31 | , totalPrivateRepoCount: ghUser.total_private_repo_count 32 | , collaborators: ghUser.collaborators 33 | , diskUsage: ghUser.disk_usage 34 | , ownedPrivateRepoCount: ghUser.owned_private_repo_count 35 | , privateGistCount: ghUser.private_gist_count 36 | , plan: { 37 | name: ghUser.plan.name 38 | , collaborators: ghUser.plan.collaborators 39 | , space: ghUser.plan.space 40 | , privateRepos: ghUser.plan.private_repos 41 | } 42 | } 43 | }; 44 | 45 | // TODO Only do this if password module is enabled 46 | // Currently, this is not a valid way to check for enabled 47 | if (everyauth.password) 48 | params[everyauth.password.loginKey()] = "github:" + ghUser.id; // Hack because of way mongodb treate unique indexes 49 | 50 | this.create(params, callback); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/modules/password/plugin.js: -------------------------------------------------------------------------------- 1 | //var crypto = require('crypto'); 2 | // 3 | //var sha = function (num) { 4 | // function secureDigest (salt, digest, password, pepper) { 5 | // return crypto 6 | // .createHash('sha' + num) // or .createHmac('sha' + num, salt) 7 | // .update('--' + Array.prototype..join.call(arguments, '--') + '--') 8 | // .digest('hex'); 9 | // } 10 | // 11 | // return { 12 | // digest: function (password, stretches, salt, pepper) { 13 | // var digest = pepper; 14 | // while (stretches--) { 15 | // digest = secureDigest(salt, digest, password, pepper); 16 | // } 17 | // return digest; 18 | // } 19 | // }; 20 | //}; 21 | // 22 | //var sha512 = sha(512); 23 | // 24 | //var sha1 = sha(1); 25 | 26 | var bcrypt = require('bcrypt') 27 | , _schema = require('./schema') 28 | , everyauth = require('everyauth') 29 | , authenticate; 30 | 31 | 32 | exports = module.exports = function (schema, opts) { 33 | schema.add(_schema); 34 | 35 | schema.virtual('password').get( function () { 36 | return this._password; 37 | }).set( function (password) { 38 | this._password = password; 39 | var salt = this.salt = bcrypt.genSaltSync(10); 40 | this.hash = bcrypt.hashSync(password, salt); 41 | }); 42 | 43 | schema.method('authenticate', function (password, callback) { 44 | bcrypt.compare(password, this.hash, callback); 45 | }); 46 | 47 | 48 | schema.static('authenticate', exports.authenticate); 49 | }; 50 | 51 | exports.authenticate = function (login, password, callback) { 52 | // TODO This will break if we change everyauth's 53 | // configurable loginName 54 | var query = {}; 55 | query[everyauth.password.loginKey()] = login; 56 | this.findOne(query, function (err, user) { 57 | if (err) return callback(err); 58 | if (!user) return callback('User with login ' + login + ' does not exist'); 59 | user.authenticate(password, function (err, didSucceed) { 60 | if (err) return callback(err); 61 | if (didSucceed) return callback(null, user); 62 | return callback(null, null); 63 | }); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /lib/modules/twitter/plugin.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | , mongooseTypes = require('mongoose-types') 3 | , _schema = require('./schema') 4 | , everyauth = require('everyauth'); 5 | mongooseTypes.loadTypes(mongoose); 6 | var Url = mongoose.SchemaTypes.Url; 7 | 8 | // See http://dev.twitter.com/doc/get/users/show 9 | module.exports = function twitter (schema, opts) { 10 | schema.add(_schema); 11 | 12 | schema.static('createWithTwitter', function (twitUserMeta, accessToken, accessTokenSecret, callback) { 13 | var params = { 14 | twit: { 15 | accessToken: accessToken 16 | , accessTokenSecret: accessTokenSecret 17 | , id: twitUserMeta.id 18 | , name: twitUserMeta.name 19 | , screenName: twitUserMeta.screen_name 20 | , location: twitUserMeta.location 21 | , description: twitUserMeta.description 22 | , profileImageUrl: twitUserMeta.profile_image_url 23 | , url: twitUserMeta.url 24 | , protected: twitUserMeta.protected 25 | , followersCount: twitUserMeta.followers_count 26 | , profileBackgroundColor: twitUserMeta.profile_background_color 27 | , profileTextColor: twitUserMeta.profile_text_color 28 | , profileLinkColor: twitUserMeta.profile_link_color 29 | , profileSidebarFillColor: twitUserMeta.profile_sidebar_fill_color 30 | , profileSiderbarBorderColor: twitUserMeta.profile_sidebar_border_color 31 | , friendsCount: twitUserMeta.friends_count 32 | , createdAt: twitUserMeta.created_at 33 | , favouritesCount: twitUserMeta.favourites_count 34 | , utcOffset: twitUserMeta.utc_offset 35 | , timeZone: twitUserMeta.time_zone 36 | , profileBackgroundImageUrl: twitUserMeta.profile_background_image_url 37 | , profileBackgroundTile: twitUserMeta.profile_background_tile 38 | , profileUseBackgroundImage: twitUserMeta.profile_use_background_image 39 | , geoEnabled: twitUserMeta.geo_enabled 40 | , verified: twitUserMeta.verified 41 | , statusesCount: twitUserMeta.statuses_count 42 | , lang: twitUserMeta.lang 43 | , contributorsEnabled: twitUserMeta.contributors_enabled 44 | } 45 | }; 46 | 47 | // TODO Only do this if password module is enabled 48 | // Currently, this is not a valid way to check for enabled 49 | if (everyauth.password) 50 | params[everyauth.password.loginKey()] = "twit:" + twitUserMeta.id; // Hack because of way mongodb treate unique indexes 51 | 52 | this.create(params, callback); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var everyauth = require('everyauth'); 2 | 3 | everyauth.everymodule.configurable({ 4 | 'User': 'A function that returns the mongoose User model (not Schema).' 5 | }); 6 | 7 | var Modules = { 8 | everymodule: require('./lib/modules/everymodule') 9 | , password: require('./lib/modules/password') 10 | , facebook: require('./lib/modules/facebook') 11 | , twitter: require('./lib/modules/twitter') 12 | , github: require('./lib/modules/github') 13 | , instagram: require('./lib/modules/instagram') 14 | , google: require('./lib/modules/google') 15 | }; 16 | 17 | // Mostly, we need this because password needs to be loaded before everything else 18 | // so that other modules can use everyauth.password.loginKey() 19 | var moduleLoadOrder = ['everymodule', 'password', 'facebook', 'twitter', 'github', 'instagram', 'google']; 20 | 21 | /** 22 | * Decorates the (User) Schema with the proper attributes. 23 | * @param {Schema} schema that gets decorated 24 | * @param {Object} options per module 25 | * @api public 26 | */ 27 | exports = module.exports = function plugin (schema, opts) { 28 | if (Object.keys(opts).length === 0) 29 | throw new Error('You must specify at least one module.'); 30 | 31 | // Make sure to flag everymodule, so that we 32 | // run the everyauth defaults for everymodule later 33 | opts.everymodule || (opts.everymodule = true); 34 | 35 | moduleLoadOrder.filter( function (moduleName) { 36 | return moduleName in opts; 37 | }).forEach( function (moduleName) { 38 | var _module = Modules[moduleName]; 39 | if (!_module) 40 | throw new Error("Missing module named " + moduleName); 41 | 42 | var decorateSchema = _module.plugin; 43 | 44 | var moduleOpts = opts[moduleName]; 45 | if (moduleOpts === true) { 46 | moduleOpts = {}; 47 | } 48 | 49 | var everyauthConfig = moduleOpts.everyauth || {}; 50 | 51 | // Module specific defaults for everyauth 52 | var everyauthDefaults = _module.everyauth; 53 | for (var k in everyauthDefaults) { 54 | if (!everyauthConfig[k]) 55 | everyauthConfig[k] = everyauthDefaults[k]; 56 | } 57 | 58 | // Configure everyauth for this module 59 | for (var k in everyauthConfig) { 60 | everyauth[moduleName][k]( everyauthConfig[k] ); 61 | } 62 | 63 | // Parse special opts 64 | var val, handler; 65 | for (var opt in moduleOpts) { 66 | if (~['everyauth', 'everymodule'].indexOf(opt)) 67 | continue; 68 | handler = _module.specialOptHandlers[opt]; 69 | if (!handler) continue; 70 | val = moduleOpts[opt]; 71 | handler(val); 72 | } 73 | 74 | decorateSchema(schema, {}); 75 | }); 76 | 77 | // Delegate middleware method to 78 | // everyauth's middleware method 79 | exports.middleware = everyauth.middleware.bind(everyauth); 80 | 81 | // Delegate helpExpress method to everyauth. 82 | // Adds dynamic helpers such as loggedIn, 83 | // accessible from the views 84 | exports.helpExpress = everyauth.helpExpress.bind(everyauth); 85 | }; 86 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | var conf = require('./conf'); 4 | 5 | var everyauth = require('everyauth') 6 | , Promise = everyauth.Promise; 7 | 8 | everyauth.debug = true; 9 | 10 | var mongoose = require('mongoose') 11 | , Schema = mongoose.Schema 12 | , ObjectId = mongoose.SchemaTypes.ObjectId; 13 | 14 | var UserSchema = new Schema({}) 15 | , User; 16 | 17 | var mongooseAuth = require('../index'); 18 | 19 | UserSchema.plugin(mongooseAuth, { 20 | everymodule: { 21 | everyauth: { 22 | User: function () { 23 | return User; 24 | } 25 | } 26 | } 27 | , facebook: { 28 | everyauth: { 29 | myHostname: 'http://local.host:3000' 30 | , appId: conf.fb.appId 31 | , appSecret: conf.fb.appSecret 32 | , redirectPath: '/' 33 | } 34 | } 35 | , twitter: { 36 | everyauth: { 37 | myHostname: 'http://local.host:3000' 38 | , consumerKey: conf.twit.consumerKey 39 | , consumerSecret: conf.twit.consumerSecret 40 | , redirectPath: '/' 41 | } 42 | } 43 | , password: { 44 | loginWith: 'email' 45 | , extraParams: { 46 | phone: String 47 | , name: { 48 | first: String 49 | , last: String 50 | } 51 | } 52 | , everyauth: { 53 | getLoginPath: '/login' 54 | , postLoginPath: '/login' 55 | , loginView: 'login.jade' 56 | , getRegisterPath: '/register' 57 | , postRegisterPath: '/register' 58 | , registerView: 'register.jade' 59 | , loginSuccessRedirect: '/' 60 | , registerSuccessRedirect: '/' 61 | } 62 | } 63 | , github: { 64 | everyauth: { 65 | myHostname: 'http://local.host:3000' 66 | , appId: conf.github.appId 67 | , appSecret: conf.github.appSecret 68 | , redirectPath: '/' 69 | } 70 | } 71 | , instagram: { 72 | everyauth: { 73 | myHostname: 'http://local.host:3000' 74 | , appId: conf.instagram.clientId 75 | , appSecret: conf.instagram.clientSecret 76 | , redirectPath: '/' 77 | } 78 | } 79 | , google: { 80 | everyauth: { 81 | myHostname: 'http://localhost:3000' 82 | , appId: conf.google.clientId 83 | , appSecret: conf.google.clientSecret 84 | , redirectPath: '/' 85 | , scope: 'https://www.google.com/m8/feeds' 86 | } 87 | } 88 | }); 89 | // Adds login: String 90 | 91 | mongoose.model('User', UserSchema); 92 | 93 | mongoose.connect('mongodb://localhost/example'); 94 | 95 | User = mongoose.model('User'); 96 | 97 | var app = express.createServer( 98 | express.bodyParser() 99 | , express.static(__dirname + "/public") 100 | , express.cookieParser() 101 | , express.session({ secret: 'esoognom'}) 102 | , mongooseAuth.middleware() 103 | ); 104 | 105 | app.configure( function () { 106 | app.set('views', __dirname + '/views'); 107 | app.set('view engine', 'jade'); 108 | }); 109 | 110 | app.get('/', function (req, res) { 111 | res.render('home'); 112 | }); 113 | 114 | app.get('/logout', function (req, res) { 115 | req.logout(); 116 | res.redirect('/'); 117 | }); 118 | 119 | mongooseAuth.helpExpress(app); 120 | 121 | app.listen(3000); 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mongoose-auth 2 | ============= 3 | 4 | User authentication plugin for mongoose node.js orm. 5 | 6 | mongoose-auth enables you to support authorization in any number of ways 7 | via authorization strategies. 8 | 9 | An authorization strategy is how you authorize your user. Currently 10 | mongoose-auth supports the following authorization strategies: 11 | 12 | - `password` 13 | - `facebook` 14 | - `twitter` 15 | - `github` 16 | - `instagram` 17 | - `google` 18 | 19 | mongoose-auth does 3 things: 20 | 21 | 1. Schema decoration 22 | 2. (optional) Drop in routing for 23 | [connect](https://github.com/senchalabs/connect) apps. 24 | 3. (optional) Dynamic helpers for 25 | [express](https://github.com/visionmedia/express) apps. 26 | 27 | It integrates the [everyauth](https://github.com/bnoguchi/everyauth) module 28 | to help it take care of the routing and helpers. everyauth is a general 29 | purpose npm module for authentication & authorization that can be used 30 | independently of mongoose. 31 | 32 | ## Schema Decoration 33 | 34 | As you add successive authorization strategies, mongoose-auth at a bare 35 | minimum augments your schema with typed attributes corresponding to parameters 36 | related to your chosen authorization strategies. For example, if facebook is 37 | one of your authorization strategies, then it will add attributes to your 38 | User Schema such as 'fb.id' and 'fb.email'. 39 | 40 | To decorate your schema: 41 | 42 | ```javascript 43 | var mongoose = require('mongoose') 44 | , Schema = mongoose.Schema 45 | , mongooseAuth = require('mongoose-auth'); 46 | 47 | var UserSchema = new Schema({}); 48 | UserSchema.plugin(mongooseAuth, { 49 | facebook: true 50 | }); 51 | ``` 52 | 53 | ## Beyond Schema Decoration: Routing 54 | 55 | Applications require more than just User Schema augmentation in order 56 | to implement a complete authorization strategy. Applications also need 57 | routes exposing the one or more steps involved for a given authorization 58 | strategy. Moreover, applications each handle in their own unique way how 59 | they want to respond to successful or failed logins (in addition to logout 60 | handling). If you are not using a 61 | [connect](https://github.com/senchalabs/connect), then you will have to 62 | set all of this up yourself. In this case, mongoose-auth *only* provides 63 | you with Schema decoration. 64 | 65 | But, if you are building your app on top of 66 | [connect](https://github.com/senchalabs/connect), then mongoose-auth 67 | provides drop in solutions for you. Here is how you can get access 68 | to the routing that mongoose-auth provides. Not the "STEP X: ..." 69 | comments: 70 | 71 | ```javascript 72 | var mongoose = require('mongoose') 73 | , Schema = mongoose.Schema 74 | , mongooseAuth = require('mongoose-auth'); 75 | 76 | var UserSchema = new Schema({}) 77 | , User; 78 | 79 | // STEP 1: Schema Decoration and Configuration for the Routing 80 | UserSchema.plugin(mongooseAuth, { 81 | // Here, we attach your User model to every module 82 | everymodule: { 83 | everyauth: { 84 | User: function () { 85 | return User; 86 | } 87 | } 88 | } 89 | 90 | , facebook: { 91 | everyauth: { 92 | myHostname: 'http://localhost:3000' 93 | , appId: 'YOUR APP ID HERE' 94 | , appSecret: 'YOUR APP SECRET HERE' 95 | , redirectPath: '/' 96 | } 97 | } 98 | }); 99 | 100 | mongoose.model('User', UserSchema); 101 | 102 | mongoose.connect('mongodb://localhost/example'); 103 | 104 | User = mongoose.model('User'); 105 | 106 | var app = express.createServer( 107 | express.bodyParser() 108 | , express.static(__dirname + "/public") 109 | , express.cookieParser() 110 | , express.session({ secret: 'esoognom'}) 111 | 112 | // STEP 2: Add in the Routing 113 | , mongooseAuth.middleware() 114 | 115 | // IMPORTANT!!!!!!! Do not add app.router, to your middleware chain 116 | // explicitly, or you will run into problems accessing `req.user` 117 | // i.e., do not use app.use(app.router). Let express do this for you 118 | // automatically for you upon your first app.get or app.post. 119 | ); 120 | 121 | // STEP 3: Add in Dynamic View Helpers (only if you are using express) 122 | mongooseAuth.helpExpress(app); 123 | 124 | app.listen(3000); 125 | ``` 126 | 127 | ## View Helpers and Convenience Methods & Getters 128 | 129 | In "Step 3" of the last code sample, we are adding dynamic view helpers, for if 130 | you are using the [Express](https://github.com/visionmedia/express) web framework. 131 | This automically gives you access to a convenient `everyauth` local variable from 132 | your view, so you do not have to pass `req` as a local to your view: 133 | 134 | - `everyauth.loggedIn` - a Boolean getter that tells you if the request is by a logged in user 135 | - `everyauth.user` - the mongoose User document associated with the session 136 | - `everyauth.facebook` - The is equivalent to what is stored at `req.session.auth.facebook`, 137 | so you can do things like ... 138 | - `everyauth.facebook.user` - returns the user json provided from the OAuth provider. 139 | - `everyauth.facebook.accessToken` - returns the access_token provided from the OAuth provider 140 | for authorized API calls on behalf of the user. 141 | - And you also get this pattern for other modules - e.g., `everyauth.twitter.user`, 142 | `everyauth.github.user`, etc. 143 | 144 | You also get access to the view helper 145 | 146 | - `user` - the same as `everyauth.user` above 147 | 148 | As an example of how you would use these, consider the following `./views/user.jade` jade template: 149 | 150 | .user-id 151 | .label User Id 152 | .value #{user.id} 153 | .facebook-id 154 | .label User Facebook Id 155 | .value #{everyauth.facebook.user.id} 156 | 157 | The "STEP 2: Add in the Routing" step in the last code sample also provides convenience methods on the 158 | `ServerRequest` instance `req`. From any scope that has access to `req`, you get the following 159 | convenience getter and method: 160 | 161 | - `req.loggedIn` - a Boolean getter that tells you if the request is by a logged in user 162 | - `req.user` - the mongoose User document associated with the session 163 | - `req.logout()` - clears the sesion of your auth data 164 | 165 | ## Using Multiple Authorization Strategies at Once 166 | 167 | You can also use multiple authorization strategies in the same application. 168 | Here is an example, using 5 authorization strategies: 169 | 170 | ```javascript 171 | // A configuration file for holding all of your 172 | // 3rd party OAuth credentials 173 | var conf = require('./conf'); 174 | UserSchema.plugin(mongooseAuth, { 175 | // Here, we attach your User model to every module 176 | everymodule: { 177 | everyauth: { 178 | User: function () { 179 | return User; 180 | } 181 | } 182 | } 183 | , facebook: { 184 | everyauth: { 185 | myHostname: 'http://localhost:3000' 186 | , appId: conf.fb.appId 187 | , appSecret: conf.fb.appSecret 188 | , redirectPath: '/' 189 | } 190 | } 191 | , twitter: { 192 | everyauth: { 193 | myHostname: 'http://localhost:3000' 194 | , consumerKey: conf.twit.consumerKey 195 | , consumerSecret: conf.twit.consumerSecret 196 | , redirectPath: '/' 197 | } 198 | } 199 | , password: { 200 | everyauth: { 201 | getLoginPath: '/login' 202 | , postLoginPath: '/login' 203 | , loginView: 'login.jade' 204 | , getRegisterPath: '/register' 205 | , postRegisterPath: '/register' 206 | , registerView: 'register.jade' 207 | , loginSuccessRedirect: '/' 208 | , registerSuccessRedirect: '/' 209 | } 210 | } 211 | , github: { 212 | everyauth: { 213 | myHostname: 'http://localhost:3000' 214 | , appId: conf.github.appId 215 | , appSecret: conf.github.appSecret 216 | , redirectPath: '/' 217 | } 218 | } 219 | , instagram: { 220 | everyauth: { 221 | myHostname: 'http://localhost:3000' 222 | , appId: conf.instagram.clientId 223 | , appSecret: conf.instagram.clientSecret 224 | , redirectPath: '/' 225 | } 226 | } 227 | }); 228 | ``` 229 | 230 | ## Example 231 | 232 | There is an example app located in [./example](https://github.com/bnoguchi/mongoose-auth/tree/master/example). 233 | To run it: 234 | 235 | $ cd example 236 | $ node server.js 237 | 238 | Then navigate to [http://localhost:3000/](http://localhost:3000) 239 | 240 | ## Recipe 1: Linking Multiple Account Logins Together 241 | 242 | A common recipe is allowing a user to login via multiple accounts *and* to link those accounts under one user 243 | document. 244 | 245 | This can be done in the following way: 246 | 247 | The real magic lies with https://github.com/bnoguchi/everyauth/, and it should be more obvious once 248 | I document everyauth more and document mongoose-auth's relationship to everyauth. 249 | 250 | In `everyauth`'s design, every auth module is defined as a set of steps, which are exposed in such a way for 251 | you to over-ride. The step that is of particular interest for this scenario is the `findOrCreateUser` step 252 | required by every `everyauth` module. `mongoose-auth` defines a default version of this `findOrCreateUser` 253 | step for each `everyauth` auth module it supports (You can find these default definitions in 254 | "lib/modules/#{moduleName}/everyauth.js" -- e.g., see 255 | [.lib/modules/facebook/everyauth.js](https://github.com/bnoguchi/mongoose-auth/tree/master/lib/modules/facebook/everyauth.js)). 256 | 257 | So for example, this is how you would over-ride the default `findOrCreateUser` step for the 258 | facebook module if you are using both the facebook and password module: 259 | 260 | ```javascript 261 | UserSchema.plugin(mongooseAuth, { 262 | facebook: { 263 | everyauth: { 264 | myHostname: ... 265 | , ... 266 | , findOrCreateUser: function (session, accessTok, accessTokExtra, fbUser) { 267 | var promise = this.Promise() 268 | , User = this.User()(); 269 | User.findById(session.auth.userId, function (err, user) { 270 | if (err) return promise.fail(err); 271 | if (!user) { 272 | User.where('password.login', fbUser.email).findOne( function (err, user) { 273 | if (err) return promise.fail(err); 274 | if (!user) { 275 | User.createWithFB(fbUser, accessTok, accessTokExtra.expires, function (err, createdUser) { 276 | if (err) return promise.fail(err); 277 | return promise.fulfill(createdUser); 278 | }); 279 | } else { 280 | assignFbDataToUser(user, accessTok, accessTokExtra, fbUser); 281 | user.save( function (err, user) { 282 | if (err) return promise.fail(err); 283 | promise.fulfill(user); 284 | }); 285 | } 286 | }); 287 | } else { 288 | assignFbDataToUser(user, accessTok, accessTokExtra, fbUser); 289 | 290 | // Save the new data to the user doc in the db 291 | user.save( function (err, user) { 292 | if (err) return promise.fail(err); 293 | promise.fuilfill(user); 294 | }); 295 | } 296 | }); 297 | }); 298 | return promise; // Make sure to return the promise that promises the user 299 | } 300 | } 301 | }); 302 | 303 | // Assign all properties - see lib/modules/facebook/schema.js for details 304 | function assignFbDataToUser (user, accessTok, accessTokExtra, fbUser) { 305 | user.fb.accessToken = accessTok; 306 | user.fb.expires = accessTokExtra.expires; 307 | user.fb.id = fbUser.id; 308 | user.fb.name.first = fbUser.first_name; 309 | // etc. more assigning... 310 | } 311 | ``` 312 | 313 | As this is a common recipe, I plan on adding support for this into `everyauth` and `mongoose-auth`, so it's more drop-in, and developers do not have to add this custom code themselves. The intent is for common things like this to be invisible to the developer, so it just *works* *like* *magic*. So, in the near future, you won't have to over-ride the findOrCreateUser step every time you want this feature. This will be coming soon. 314 | 315 | ## Recipe 2: Configuring Email or Phone to be your Login for the Password Module 316 | 317 | By default, `everyauth` and therefore `mongoose-auth` use the attribute `login` as the default attribute used for logging in 318 | with the password module. 319 | 320 | However, the need can arise to use a different attribute (such as email) that implies a different schema (use `email: String` instead of `login: String`) 321 | in addition to different validation assumptions (email validations are more strict that login handle validations). 322 | 323 | Luckily, `mongoose-auth` provide support for this out of the box. All you need to do is (look for the line labeled "THIS NEXT LINE IS THE ONLY ADDITION"): 324 | 325 | ```javascript 326 | UserSchema.plugin(mongooseAuth, { 327 | // Here, we attach your User model to every module 328 | everymodule: { 329 | everyauth: { 330 | User: function () { 331 | return User; 332 | } 333 | } 334 | } 335 | , password: { 336 | // THIS NEXT LINE IS THE ONLY ADDITION 337 | loginWith: 'email' // Or loginWith: 'phone' 338 | 339 | , everyauth: { 340 | getLoginPath: '/login' 341 | , postLoginPath: '/login' 342 | , loginView: 'login.jade' 343 | , getRegisterPath: '/register' 344 | , postRegisterPath: '/register' 345 | , registerView: 'register.jade' 346 | , loginSuccessRedirect: '/' 347 | , registerSuccessRedirect: '/' 348 | } 349 | } 350 | }); 351 | ``` 352 | 353 | Automatically, `mongoose-auth` will use an `email` String attribute in your User schema 354 | instead of the default `login` String attribute. Moreover, it will automatically add in 355 | validation checks to make sure that the email is valid before registering a user through 356 | the registration process of the password module. 357 | 358 | ## Recipe 3: Extra password registration data besides login + password 359 | 360 | Sometimes your registration will ask for more information from the user besides the login and password. 361 | 362 | For this particular scenario, you can configure `extraParams`. 363 | 364 | ```javascript 365 | UserSchema.plugin(mongooseAuth, { 366 | // Here, we attach your User model to every module 367 | everymodule: { 368 | everyauth: { 369 | User: function () { 370 | return User; 371 | } 372 | } 373 | } 374 | , password: { 375 | extraParams: { 376 | phone: String 377 | , name: { 378 | first: String 379 | , last: String 380 | } 381 | } 382 | 383 | , everyauth: { 384 | getLoginPath: '/login' 385 | , postLoginPath: '/login' 386 | , loginView: 'login.jade' 387 | , getRegisterPath: '/register' 388 | , postRegisterPath: '/register' 389 | , registerView: 'register.jade' 390 | , loginSuccessRedirect: '/' 391 | , registerSuccessRedirect: '/' 392 | } 393 | } 394 | }); 395 | ``` 396 | 397 | What this effectively does is: 398 | 399 | 1. Adds `phone`, `name.first`, and `name.last` as attributes to your `UserSchema`. 400 | 2. Automatically extracts the registration parameters after a visitor submits the registration 401 | form and saves them to a new `User` document. 402 | The registration form `` `name`s should be, e.g., in the example above: 'phone', 403 | 'name[first]', and 'name[last]'. 404 | 405 | Please see [./example/server.js](https://github.com/bnoguchi/mongoose-auth/tree/master/example/server.js#L45) 406 | for a live example. 407 | 408 | ## Recipe 4: Adding more attributes to your schema 409 | 410 | This one ha come up enough that it is here as a recipe, even though it is not specific to `mongoose-auth`. Suppose 411 | you want to add a special attribute such as `roles: [String]` to your UserSchema. This is something that you can do 412 | using just `mongoose` 413 | 414 | ```javascript 415 | var UserSchema = new mongoose.Schema({ 416 | roles: [String] 417 | , // other custom attributes 418 | }); 419 | 420 | UserSchema.plugin(mongooseAuth, { 421 | // mongooseAuth *adds* other attributes to your UserSchema 422 | // depending on the auth modules you choose. 423 | }); 424 | ``` 425 | 426 | ## Recipe 5: Customizing how you do password login authentication 427 | 428 | Currently, `mongoose-auth` does password authentication by login and password. Suppose you also want to authenticate 429 | by checking against an additional parameter, like `active`, which is a Boolean attribute on your UserSchema that 430 | indicates whether this user has been activated or not. Then you can modify the `authenticate` everyauth step in the 431 | following way: 432 | 433 | ```javascript 434 | var UserSchema = new Schema({ 435 | active: Boolean 436 | }), User; 437 | UserSchema.plugin(mongooseAuth, { 438 | everymodule: { 439 | everyauth: { 440 | User: function () { 441 | return User; 442 | } 443 | } 444 | } 445 | , password: { 446 | loginWith: 'email' 447 | , everyauth: { 448 | getLoginPath: '/login' 449 | , postLoginPath: '/login' 450 | , loginView: 'login.jade' 451 | , getRegisterPath: '/register' 452 | , postRegisterPath: '/register' 453 | , registerView: 'register.jade' 454 | , loginSuccessRedirect: '/' 455 | , registerSuccessRedirect: '/' 456 | 457 | // WHAT YOU ADD IS THE FOLLOWING: 458 | // The logic is adapted from the default authenticate 459 | // implementation in lib/modules/password/everyauth.js 460 | , authenticate: function (login, password) { 461 | var promise 462 | , errors = []; 463 | if (!login) errors.push('Missing login.'); 464 | if (!password) errors.push('Missing password.'); 465 | if (errors.length) return errors; 466 | 467 | promise = this.Promise(); 468 | this.User()().authenticate(login, password, function (err, user) { 469 | if (err) { 470 | errors.push(err.message || err); 471 | return promise.fulfill(errors); 472 | } 473 | if (!user) { 474 | errors.push('Failed login.'); 475 | return promise.fulfill(errors); 476 | } 477 | 478 | // The following block is the new code 479 | if (!user.active) { 480 | errors.push('You are not yet activated.'); 481 | return promise.fulfill(errors); 482 | } 483 | 484 | promise.fulfill(user); 485 | }); 486 | return promise; 487 | } 488 | } 489 | } 490 | }); 491 | mongoose.model('User', UserSchema); 492 | 493 | User = mongoose.model('User'); 494 | ``` 495 | 496 | ## Recipe 6: Customizing logout handler 497 | 498 | This is a copy of instructions from `everyauth` and applied to `mongoose-auth`: 499 | 500 | ```javascript 501 | // ... 502 | UserSchema.plugin(mongooseAuth, { 503 | everymodule: { 504 | everyauth: { 505 | User: function () { 506 | return User; 507 | }, 508 | handleLogout: function(req, res) { 509 | // Put your extra logic here 510 | req.logout(); // The logout method is added for you by everyauth, too 511 | // And/or put your extra logic here 512 | res.writeHead(303, { 'Location': this.logoutRedirectPath() }); 513 | res.end(); 514 | } 515 | } 516 | } 517 | // ... 518 | }); 519 | // ... 520 | ``` 521 | 522 | 523 | 524 | ### License 525 | MIT License 526 | 527 | --- 528 | ### Author 529 | Brian Noguchi 530 | --------------------------------------------------------------------------------