MEAN Stack : Authentication with Passport 2
This tutorial is not much different from the previous one: Authentication with Passport.
In this tutorial, we will use the Local Authentication Strategy of Passport and authenticate the users against a locally configured Mongo DB instance.
Let's generate a boilerplate application by simply executing:
$ express passport-local2
Let's install dependencies:
{ "name": "passport-local2", "version": "0.0.1", "private": true, "scripts": { "start": "node ./bin/www" }, "repository": { "type": "git", "url": "git@github.com:epic-math/NodeJS-MEAN.git" }, "author": "K Hong <k@epicmath.com>", "license": "MIT", "dependencies": { "body-parser": "~1.13.2", "cookie-parser": "~1.3.5", "debug": "~2.2.0", "express": "~4.2.0", "jade": "~1.11.0", "morgan": "~1.6.1", "serve-favicon": "~2.3.0", "passport": "^0.2.0", "passport-local": "^1.0.0", "passport-local-mongoose": "^1.0.0", "mongoose": "~3.8.12", "express-session": "~1.11.3", "bcrypt-nodejs" : "*", "connect-flash" : "*" } }
With the package.json, let's install dependencies:
$ pwd /home/ubuntu/passport-local2 $ sudo npm install
Note that we added the dependencies for passport and passport-local module.
Let's create a User Model in Mongoose which we can perform CRUD operations on the underlying database. The user model will be saved in models/user.js:
Let's install MongoDB:
var mongoose = require('mongoose'); module.exports = mongoose.model('User',{ username: String, password: String, email: String, gender: String, address: String });
Here is the updated app.js:
var mongoose = require('mongoose'); ... // mongoose mongoose.connect('mongodb://localhost/passport_local_mongoose_express4');
Passport provides the mechanism to handle authentication leaving the onus of implementing session-handling ourselves and for that we will be using express-session. Let's modify app.js:
// Configuring Passport var passport = require('passport'); var expressSession = require('express-session'); app.use(expressSession({secret: 'mySecret'})); app.use(passport.initialize()); app.use(passport.session());
This is needed as we want our user sessions to be persistent in nature. Before running the app, we need to install express-session and add it to our dependency list in package.json:
$ npm install --save express-session
Passport also needs to serialize and deserialize user instance from a session store in order to support login sessions, so that every subsequent request will not contain the user credentials. It provides two methods serializeUser and deserializeUser for this purpose:
passport.serializeUser(function(user, done) { done(null, user._id); }); passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); }); });
We will now define Passport's strategies for handling login. It would be an instance of the Local Authentication Strategy of Passport and would be created using the passport.use() function. We use connect-flash to help us with error handling by providing flash messages which can be displayed to user on error.
var LocalStrategy = require('passport-local').Strategy; var User = require('../models/user'); var bCrypt = require('bcrypt-nodejs'); module.exports = function(passport){ passport.use('login', new LocalStrategy({ passReqToCallback : true }, function(req, username, password, done) { // check in mongo if a user with username exists or not User.findOne({ 'username' : username }, function(err, user) { // In case of any error, return using the done method if (err) return done(err); // Username does not exist, log the error and redirect back if (!user){ console.log('User Not Found with username '+username); return done(null, false, req.flash('message', 'User Not found.')); } // User exists but wrong password, log the error if (!isValidPassword(user, password)){ console.log('Invalid Password'); return done(null, false, req.flash('message', 'Invalid Password')); // redirect back to login page } // User and password both match, return user from done method // which will be treated like success return done(null, user); } ); }) ); var isValidPassword = function(user, password){ return bCrypt.compareSync(password, user.password); } }
The first parameter to passport.use() is the name of the strategy which will be used to identify this strategy when applied later. The second parameter is the type of strategy that you want to create, here we use the username-password or the LocalStrategy.
passport.use('login', new LocalStrategy({ passReqToCallback : true },
It is to be noted that by default the LocalStrategy expects to find the user credentials in username & password parameters, but it allows us to use any other named parameters as well. The passReqToCallback config variable allows us to access the request object in the callback, thereby enabling us to use any parameter associated with the request.
Next, we use the Mongoose API to find the User in our underlying collection of Users to check if the user is a valid user or not. The last parameter in our callback : done denotes a useful method using which we could signal success or failure to Passport module.
function(req, username, password, done) { ... );
To specify failure either the first parameter should contain the error, or the second parameter should evaluate to false. To signify success the first parameter should be null and the second parameter should evaluate to a true value, in which case it will be made available on the request object.
Since passwords are inherently weak in nature, we should always encrypt them before saving them to the database. For this, we use bCrypt to help us out with encryption and decryption of passwords.
var isValidPassword = function(user, password){ return bCrypt.compareSync(password, user.password); }
Now we want to define the next strategy which will handle registration of a new user entry in our underlying Mongo DB:
passport.use('signup', new LocalStrategy({ passReqToCallback : true }, function(req, username, password, done) { findOrCreateUser = function(){ // find a user in Mongo with provided username User.findOne({'username':username},function(err, user) { // In case of any error return if (err){ console.log('Error in SignUp: '+err); return done(err); } // already exists if (user) { console.log('User already exists'); return done(null, false, req.flash('message','User Already Exists')); } else { // if there is no user with that email // create the user var newUser = new User(); // set the user's local credentials newUser.username = username; newUser.password = createHash(password); newUser.email = req.param('email'); newUser.firstName = req.param('firstName'); newUser.lastName = req.param('lastName'); // save the user newUser.save(function(err) { if (err){ console.log('Error in Saving user: '+err); throw err; } console.log('User Registration succesful'); return done(null, newUser); }); } }); }; // Delay the execution of findOrCreateUser and execute // the method in the next tick of the event loop process.nextTick(findOrCreateUser); }); );
We now define our routes for the application in routes/index.js module which takes the instance of Passport created in app.js:
var express = require('express'); var router = express.Router(); var isAuthenticated = function (req, res, next) { // if user is authenticated in the session, call the next() to call the next request handler // Passport adds this method to request object. A middleware is allowed to add properties to // request and response objects if (req.isAuthenticated()) return next(); // if the user is not authenticated then redirect him to the login page res.redirect('/'); } module.exports = function(passport){ /* GET login page. */ router.get('/', function(req, res) { // Display the Login page with any flash message, if any res.render('index', { message: req.flash('message') }); }); /* Handle Login POST */ router.post('/login', passport.authenticate('login', { successRedirect: '/home', failureRedirect: '/', failureFlash : true })); /* GET Registration Page */ router.get('/register', function(req, res){ res.render('register',{message: req.flash('message')}); }); /* Handle Registration POST */ router.post('/register', passport.authenticate('register', { successRedirect: '/home', failureRedirect: '/register', failureFlash : true })); /* GET Home Page */ router.get('/home', isAuthenticated, function(req, res){ res.render('home', { user: req.user }); }); /* Handle Logout */ router.get('/signout', function(req, res) { req.logout(); res.redirect('/'); }); return router; }
We're using passport.authenticate() to delegate the authentication to login and register strategies when a HTTP POST is made to /login and /register routes respectively.
doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css') body block content
extends layout block content div.container div.row div.col-sm-6.col-md-4.col-md-offset-4 h1.text-center.login-title Sign in to our Passport app div.account-wall img(class='profile-img', src='https://lh5.googleusercontent.com/-b0-k99FZlyE/AAAAAAAAAAI/AAAAAAAAAAA/eu7opA4byxI/photo.jpg?sz=120') form(class='form-signin', action='/login', method='POST') input(type='text', name='username' class='form-control', placeholder='Email',required, autofocus) input(type='password', name='password' class='form-control', placeholder='Password', required) button(class='btn btn-lg btn-primary btn-block', type='submit') Sign in span.clearfix a(href='/signup', class='text-center new-account') Create an account #message if message h1.text-center.error-message #{message}
Click "Create an account":
Click "Register":
Click "Sign Out":
If we try a user that's already been created:
Source available at https://github.com/epic-math/NodeJS-MEAN.
Here is the list of references:
Node.JS
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization