Authentication Middleware: Passport, a Tutorial

The Application and the Models

We create a remote repo by the name node_passport_login and then do the following locally. Notice the use of the Express CLI inreface to create the application. This ensures uniformity between projects. The structure will be familiar from one project to the next. Our lives will be easier.

express --view=pug node_passport_login
cd node_passport_login
npm install
git init
git add .
git commit -m 'first commit'
git remote add origin <name from remote git>
git push -u origin master
npm start

This should of course result in the normal opening Welcome to Express. If it does we are ready to roll. Now we do

npm i mongoose
npm i passport passport-local
npm i express-session
npm i bcryptjs connect-flash

Now edit yout app.js so that it holds

Example 48.1. The Application, node_passport_login/app.js
const createError = require('http-errors');
const path = require('path');
const logger = require('morgan');

const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const flash = require('connect-flash');
const session = require('express-session');

// Passport Config
require('./config/passport')(passport);

// DB Config and server connect
const db = require('./config/keys').mongoURI;
mongoose.connect('mongodb://localhost/node-auth', {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true
    })
    .then( function() { console.log('mongoose connection open'); })
    .catch( function(err) { console.error(err); });

const app = express();
app.locals.pretty = app.get('env') === 'development';       // pretty print html

// view engine setup pug and static
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

// Express body parser
app.use(express.urlencoded({ extended: true }));
app.use(logger('dev'));

// Express session
app.use(require('express-session')({                        // passport initialize
    secret: 'ioeruir!rznbzvmn8768576hdsw&%',                 // do the keyboard cat
    resave: true,                                           // to create entropy
    saveUninitialized: false
}));

// Passport middleware
app.use(passport.initialize());
app.use(passport.session());

// Connect flash
app.use(flash());

// Global variables
app.use(function(req, res, next) {
  res.locals.success_msg = req.flash('success_msg');
  res.locals.error_msg = req.flash('error_msg');
  res.locals.error = req.flash('error');
  next();
});

// Routes
app.use('/', require('./routes/index.js'));
app.use('/users', require('./routes/users.js'));

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

The codelines 15 through 22 sets up access to the MongoDB software, and opens a connection to the database server. This means that the connection will remain open throughout the duration of the lifespan of the web server.

The codelines 43-45 gets access to the Passport software, and establishes the authentication of this web application.

Now create the directory models, and in it the file

Example 48.2. The User, node_passport_login/models/User.js
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    unique: true
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
  date: {
    type: Date,
    default: Date.now
  }
});

const User = mongoose.model('User', UserSchema, 'user');

module.exports = User;

The Control Layer

In this tutorial we see a slightly different architecture than we have used previously. Until now we have mixed routing and controller code somewhat. This may be done when applications are relatively small. An application of scale may be better served with a separate appplication logic layer, the controller layer. Routing as we shall see now, shrinks to a terse "with this route the controller is <some function name>"

Example 48.3.  The Router, node_passport_login/routes/index.js
const express = require('express');
const router = express.Router();
const { ensureAuthenticated, forwardAuthenticated } = require('../config/auth');
const idx = require("../controllers/indexController");



router.get('/', forwardAuthenticated, idx.frontpage);



router.get('/dashboard', ensureAuthenticated, idx.dashboard);

module.exports = router;

Example 48.4.  The User Router, node_passport_login/routes/users.js
const express = require('express');
const router = express.Router();
const auth = require("../controllers/authController.js");
const { forwardAuthenticated } = require('../config/auth');



router.get('/register', forwardAuthenticated, auth.register);
router.post('/register', auth.postRegister);



router.get('/login', forwardAuthenticated, auth.login);
router.post('/login', auth.postLogin);



router.get('/logout', auth.logout);

module.exports = router;

We notice that some of the routers have more than one callback functions. This is documented in the Express documentation. Whatever callback that is not last in the chain must end with a return next(); to pass control to the next callback function in the chain. This is shown further down in Example 48.8.

On larger sites we use a controller per subject matter area. This is exemplified here by a controller for regular functionalities, and one for user matters i.e. resgistration and login, also known as authentication matters. Perhaps at this point it is convenient to show the directory structure for this "kind of best practice" project:

Example 48.5. Project Structure, Overview
.
├── README.md
├── app.js
├── bin
│   └── www
├── config
│   ├── auth.js
│   ├── keys.js
│   └── passport.js
├── controllers
│   ├── authController.js
│   └── indexController.js
├── models
│   └── User.js
├── node_modules                         contents of node_modules
...                                      not shown
├── package-lock.json
├── package.json
├── public
│   ├── favicon.svg
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── dashboard.pug
    ├── error.pug
    ├── index.pug
    ├── layout.pug
    ├── login.pug
    ├── register.pug
    └── regsave.pug

Example 48.6.  The Non Authentication, Regular Controller, node_passport_login/controllers/indexController.js
exports.frontpage = function (req, res) {
    res.render('index', {
        title: 'Demoing PassportJS',
        subtitle: 'Inspired by Traversy'
    });
};

exports.dashboard = function (req,res) {
    res.render('dashboard', {
        title: 'Demoing PassportJS',
        subtitle: 'Here\'s What We Do:',
        user: req.user
    });
};

Example 48.7.  The Authentication Controller, node_passport_login/controllers/authController.js
const bcrypt = require('bcryptjs');
const passport = require('passport');
const mongoose = require('mongoose');

const User = require('../models/User');
const saltRounds = 10;

exports.register = function (req, res) {
    res.render('register', {
            title: 'Demoing PassportJS',
            subtitle: 'Inspired by Traversy'
    });
};

exports.postRegister = function (req, res) {
    const { name, email, password, password2 } = req.body;
    let errors = [];

    if (!name || !email || !password || !password2) {
        errors.push({ msg: 'Please enter all fields' });
    }

    if (password != password2) {
        errors.push({ msg: 'Passwords do not match' });
    }

    if (password.length < 12) {
        errors.push({ msg: 'Password must be at least 12 characters' });
    }

    if (errors.length > 0) {
        res.render('register', {
            errors,
            name,
            email,
            password,
            password2
        });
    } else {
        User.findOne({ email: email }).then( function (user) {
            if (user) {
                errors.push({ msg: 'Email already exists' });
                res.render('register', {
                    errors,
                    name,
                    email,
                    password,
                    password2
                });
              } else {
                  const newUser = new User({
                      name,
                      email,
                      password
                  });

                  bcrypt.hash(newUser.password, saltRounds, function (err, hash) {
                      if (err) throw err;
                      newUser.password = hash;
                      newUser
                          .save()
                          .then(user => {
                              req.flash(
                                  'success_msg',
                                  'You are now registered and can log in'
                              );
                              res.redirect('/users/login');
                          })
                          .catch(err => console.log(err));
                  });
              }
        });
    }
};

exports.login = function (req, res) {
    res.render('login', {
        title: 'Demoing PassportJS',
        subtitle: 'Inspired by Traversy'
    });
};

exports.postLogin = function (req, res, next) {
    passport.authenticate('local', {
        successRedirect: '/dashboard',
        failureRedirect: '/users/login',
        failureFlash: true
    })(req, res, next);
};

exports.logout = function (req, res) {
    req.logout();
    req.flash('success_msg', 'You are logged out');
    res.redirect('/users/login');
};

Example 48.8.  Configuration, The Saint Peter of the Application node_passport_login/config/auth.js

Referred to from the routers as guards of the pages only authenticated users have access to.

module.exports = {
    ensureAuthenticated: function(req, res, next) {
        if (req.isAuthenticated()) {
            return next();
        }
        req.flash('error_msg', 'Please log in to view that resource');
        res.redirect('/users/login');
    },

    forwardAuthenticated: function(req, res, next) {
        if (!req.isAuthenticated()) {
            return next();
        }
        res.redirect('/dashboard');
    }
};

Example 48.9.  Configuration, node_passport_login/config/passport.js

This file is required for Passport use.

const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');

// Load User model
const User = require('../models/User');

module.exports = function(passport) {
    passport.use(
        new LocalStrategy({ usernameField: 'email' }, function (email, password, done) {
            // Match user
            User.findOne({
                email: email
            }).then(function (user) {
                if (!user) {
                    return done(null, false, { message: 'That email is not registered' });
                }

                // Match password
                bcrypt.compare(password, user.password, function (err, isMatch) {
                    if (err) throw err;
                    if (isMatch) {
                        return done(null, user);
                    } else {
                        return done(null, false, { message: 'Password incorrect' });
                    }
                });
            });
        })
    );

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

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

Example 48.10.  Configuration, node_passport_login/config/keys.js

This file is required somewhere, but none of its content is used in the current project.

dbPassword = 'mongodb+srv://YOUR_USERNAME_HERE:'+ encodeURIComponent('YOUR_PASSWORD_HERE') + '@CLUSTER_NAME_HERE.mongodb.net/test?retryWrites=true';

module.exports = {
    mongoURI: dbPassword
};

The Views

Example 48.11.  The Common Part, node_passport_login/views/layout.pug
doctype
html
  head
    title= title
    meta(charset='utf-8')
    meta(name="viewport" content="width=device-width, initial-scale=1.0")
    link(rel="icon", type="image/svg+xml", href="/favicon.svg")
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type="module", src='/javascripts/nquery.js')
  body
    header
      h2= title
      nav
        ul
          li
            a(href="/") Home
          if user
            li
              a(href="/dashboard") Insiders
            li
              a(href="/users/logout") Logout
          if !user
            li
              a(href="/users/register") Register
            li
              a(href="/users/login") Login
    main
      block content
    footer
        div(id="cpy")
        div
          a(href="#") twitter

Example 48.12.  The Front Page, node_passport_login/views/index.pug
extends layout

block content
  h1= subtitle
  p Welcome to #{title}

Example 48.13.  The Register Page, node_passport_login/views/register.pug
extends layout

block content
    h1 Register
    if errors != undefined && errors.length != 0
        each error in errors
            div(role='alert' id='a1') #{error.msg}
    if success_msg != ''
        div(role='alert' id='a2') #{success_msg}
    if error_msg != ''
        div(role='alert' id='a3') #{error_msg}
    if error != ''
        div(role='alert' id='a4') #{error}
    form(id='myformr' action='/users/register' method='post')
        table
            tr
                td Name:
                td
                    input(type='name' name='name' placeholder='firstname' required)
            tr
                td Email:
                td
                    input(type='email' name='email' placeholder='n@mm.tld' required)
            tr
                td Password:
                td
                    input(type='password' name='password' placeholder='password, minimum 12 mixed characters' required)
            tr
                td Password confirm:
                td
                    input(type='password' name='password2' placeholder='repeat password' required)
            tr
                td
                td
                    input(type='submit' value='Go')
    p Have an account?&nbsp;
        a(href='/users/login') Login

Example 48.14.  The Login Page, node_passport_login/views/login.pug
extends layout

block content
    h1 Login
    if errors != undefined && errors.length != 0
        each error in errors
            div(role='alert' id='a1') #{error.msg}
    if success_msg != ''
        div(role='alert' id='a2') #{success_msg}
    if error_msg != ''
        div(role='alert' id='a3') #{error_msg}
    if error != ''
        div(role='alert' id='a4') #{error}
    form(id='myforml' action='/users/login' method='post')
        table
            tr
                td Email:
                td
                    input(type='text' name='email' required)
            tr
                td Password:
                td
                    input(type='password' name='password' required)
            tr
                td
                td
                    input(type='submit' value='Go')
    p No account?&nbsp;
      a(href='/users/register') Register

Example 48.15.  The Dashboard Page, node_passport_login/views/dashboard.pug
extends layout

block content
  h1= subtitle
  p Welcome to #{user.name}