Passport : LocalStrategy Rundown

Passport is an authentication middleware for nodejs. It can either authenicate locally ( user signup/login ) or it can use 3rd parties to authenticate through OAUTH (fb, twitter, g+ …). A simple authentication example would be, to view X page, a user must be logged in, or else they are redirected to the home page.

In this blog post I am going to start from a fresh express app, and show how easy it is to use passport for local authentication. In future posts we will cover 3rd party authentication.


The App Setup

file structure

passport_example(dir)
---- app
------ routes.js
------ models
-------- users.js
---- config
-------- passport.js
---- public
---- views
-------- layout.hbs
-------- index.hbs
-------- login.hbs
-------- signup.hbs
-------- profile.hbs
---- server.js
---- package.json

we will put our routes and models(using mongoose & mongodb for persistence/ORM), config will contain our passport configuration which is where basically all of our passport code will go. Public for the css, frontend js and images. Views for our templates(handlebars), and server.js for our entry point.

package.json

{
  "name": "passportexample",
  "version": "1.0.0",
  "main": "server.js",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt-nodejs": "0.0.3",
    "body-parser": "^1.14.1",
    "connect-flash": "^0.1.1",
    "cookie-parser": "^1.4.0",
    "express": "^4.13.3",
    "express-session": "^1.12.1",
    "hbs": "^4.0.0",
    "mongoose": "^4.2.5",
    "morgan": "^1.6.1",
    "passport": "^0.3.0",
    "passport-local": "^1.0.0",
    "path": "^0.12.7"
  }
}

server.js

var express      = require('express');
var app          = express();
var port         = process.env.PORT || 3000;
var path         = require('path');
var mongoose     = require('mongoose');
var passport     = require('passport');
var flash        = require('connect-flash');
var morgan       = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser   = require('body-parser');
var session      = require('express-session');
var hbs          = require('hbs');

// DB
var dburl = process.env.MONGOLAB_URI || 'mongodb://localhost:27017/ppexample2';
mongoose.connect(dburl);
mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
mongoose.connection.once('open', function() { console.log("Mongo DB connected!"); });

// PASSPORT
require('./config/passport.js')(passport);

// MIDDLEWARE
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');
app.use(morgan('dev'));
app.use(cookieParser());
app.use(bodyParser());
app.use(session({ secret: 'supersecret', name: 'barnibus' }));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());

// ROUTES
require('./app/routes.js')(app, passport);

// LISTEN
app.listen(port);
console.log('magic happening on port ' + port);

I am assuming you already have a basic understanding of node, and hopfully most of the following looks familiar. We are setting up our DB, loading in config/passport and passing it the passport object so we can make modifications to it and the modifications will be availible on the instance of it when we later pass it into our routes when we require it. After we load passport we are setting up our middleware, making sure to initiate passport, and passport sessions, as well as flash so we can use flash messages. Next our routes, which we already talked about, then starting the server.

Lets just add something into routes.js and passport.js so when we want to fire up the server we are sure everything is working.

app/routes.js

module.exports = function(app, passport) {
  // body...
}

config/passport.js

module.exports = function(passport) {
  // body...
}


Next lets create the basic frontend. I will not being adding much style, if any, so we wont get distracted.

views/layout.hbs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>passport example</title>

  <style>
    body {
      width: 1000px;
      margin: 40px auto;
      font: Arial;
      font-size: 14px;
    }
  </style>

</head>
<body>

<div id="main">
  <h1>Passport Example App</h1>

  }
</div>

</body>
</html>

views/index.hbs

<h3>welcome home...</h3>
<a href="/login">Login</a>
<a href="/signup">Signup</a>

views/login.hbs

<h3>Login Page</h3>


  <span class="alert" style="background-color: red;"></span>


<form action="/login" method="post">
  <input type="email" name="email" placeholder="email..."><br>
  <input type="password" name="password" placeholder="password...">
  <input type="submit" value="Submit">
</form>

views/signup.hbs


<h3>Signup Page</h3>


  <span class="alert" style="background-color: red;"></span>


<form action="/signup" method="post">
  <input type="email" name="email" placeholder="email..."><br>
  <input type="password" name="password" placeholder="password...">
  <input type="submit" value="Submit">
</form>

Lets create the model for our users object.

app/models/users.js

var mongoose = require('mongoose');
var bcrypt = require('bcrypt-nodejs');

var userSchema = new mongoose.Schema({
  local: {
    email: String,
    password: String
  }
});

userSchema.methods.generateHash = function(password) {
  return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
}

userSchema.methods.validPassword = function(password) {
  return bcrypt.compareSync(password, this.local.password);
}

module.exports = mongoose.model('User', userSchema);

we will be adding more to this in future posts show we can have, for example, twitter: { ... } on user also.

Lastly, before we move on, lets add some routes and make sure our app is working.

app/routes.js

module.exports = function(app, passport) {

  app.get('/login', function(req, res) {
    res.render('login', { message: req.flash('loginMessage') });
  });

  app.get('/signup', function(req, res) {
    res.render('signup', { message: req.flash('signupMessage') });
  });

  app.post('/login', function(req, res) {
    console.log('posted to login: ', req.body);
  });

  app.post('/signup', function(req, res) {
    console.log('posted to signup: ', req.body);
  });

  app.get('/', function(req, res) {
    res.render('index');
  });

}

Now lets try to fire the app up. Install all the deps: $ npm install. Fire up mongod: $ mongod. Run $ nodemon server.js. The app should we running and you can view it at localhost:3000. If you go to the signup/login page and enter your info then click submit, you should see the values of the email and password input spit out into an object in the console running your server because of console.log('...', req.body).

We are now done setting up the app, and now time to proceed to the passport fun!


The Passport Code

local-signup

Now with our app set up, ready for the passport code, we will add the following

config/passport.js

var User = require('../app/models/users');

module.exports = function(passport) {

  passport.serializeUser(function(user, done) {
    done(null, user.id);
  });

  passport.deserializeUser(function(id, done) {
    User.findById(id, function(err, user) {
      done(err, user);
    });
  });

  // stategies will go here...

}

the serial/deserializeUsers is necessary to give the requestor a cookie containing the id of the user for authentication. This will be nearly redundant with each vary localStrategy to be used.

now we will add the local-signup strategy for our passport config

config/passport.js


var User = require('../app/models/users');
var LocalStrategy = require('passport-local').Strategy;

module.exports = function(passport) {

  // serialize/deserialize User

  passport.use('local-signup', new LocalStrategy({

    usernameField: 'email',
    passwordField: 'password',
    passReqToCallback: true

  }, function(req, email, password, done) {

    process.nextTick(function() { // for async

      User.findOne({ 'local.email': email }, function(err, user) {
        if (err) { return done(err); }
        if (user) {
          return done(null, false, req.flash('signupMessage', 'That email already exists.'));
        } else {
          var newUser = new User();
          newUser.local.email = email;
          newUser.local.password = newUser.generateHash(password);
          newUser.save(function(err) {
            if (err) { throw err; }
            return done(null, newUser);
          });
        }
      });

    });

  }));

}

we are loading in the User model as well as the LocalStrategy. We are defining the local-signup to override the username field with an email field, and passToCallback ensure req will be included in the callback that follows so we can use req.flash.

process.nextTick(fun) ensures the request is made before the DB query executes, then we are trying to find a User in the DB. If a user is found, we need to not allow request to continue and display signupMessage. If no User is found, create a new one, returning the callback once the user successfully saves.

Now that we have our local-signup strategy setup, lets add the changes to our route for app.post(‘/signup’)

// other routes ...

  app.post('/signup', passport.authenticate('local-signup', {
    successRedirect: '/profile',
    failureRedirect: '/signup',
    failureFlash: true
  }));

  app.get('/profile', isLoggedIn, function(req, res) {
    res.render('profile');
  });

} // end of module.exports

function isLoggedIn(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  } else {
    res.redirect('/')
  }
}

we also added a route for /profile so when successfully redirected we have a route. We also added a middleware function we can pass to our routes to ensure a user is logged in to view a route, or they will be redirected to the index page.

Now if we try to login it should work. We can check our mongodb by entering mongo console, use ppexample2, which is the name we gave our mongo server in the config in server.js. db.users.find() should show you your record, with a hashed password. You always want to hash your passwords that are being saved in the DB in case your DB in compromised, the compromiser will only have a random string for your password.

If needed, db.dropDatabase() to drop the DB if somehow the info was saved incorrectly. Now lets set up our local-login.

local-login


  // serialize/deserialize User, local-signup ...

  passport.use('local-login', new LocalStrategy({

    usernameField: 'email',
    passwordField: 'password',
    passReqToCallback: true

  }, function(req, email, password, done) {

    User.findOne({ 'local.email': email }, function(err, user) {
      if (err) { return done(err); }
      if (!user) { return done(null, false, req.flash('loginMessage', 'User not found.')); }
      if (!user.validPassword(password)) {
        return done(null, false, req.flash('loginMessage', 'Incorrect password.'));
      } else {
        return done(null, user);
      }
    });

  }));

}

A little different from the local-signup, we are checking for a user by email, if there is no user, return req.flash error message. If the password is invalid ( userSchema.methods.validPassword(pw) defined in user model ), we do the same but with an incorrect password. If all the above dont occur, the callback will be returned with the found user object.

now lets update the route

app/routes.js

// ... prev routes
app.post('/login', passport.authenticate('local-login', {
  successRedirect: '/profile',
  failureRedirect: '/login',
  failureFlash: true
}));

Restart the server, and now if you try to login with the same credential as you used previously to test out local-signup, you should be redirected to the /profile page!