How to authenticate users with Node.js using Passport.js(passport-local)

Prerequisites

To follow along with this tutorial, you’ll need to have Node and MongoDB installed on your machine. You can install Node by heading to the official Node download page and grabbing the correct binaries for your system.

Also, here is a link to download mongodb.

Creating the Project

Once all of the prerequisite software is set up, we can get started. We’ll begin by creating the folder for our app and then accessing that folder on the terminal:

mkdir Auth
cd Auth

To create the node app, we’ll use the following command:

npm init -y

Now, we'll need to install express and other dependencies:

npm i express bcryptjs body-parser connect-flash ejs express-session mongoose passport passport-local validatorjs

Now let's configure babel to make use of es6 syntax

npm i @babel/core @babel/node --save-dev

Then we create a file .babelrc in the root of our project folder and paste:

{
  "presets": ["@babel/preset-env"]
}

When that’s done, create an index.js file in the root folder of your app and add the following content to it:

import express from 'express';
import path from 'path';
import ejs from 'ejs';
import flash from 'connect-flash';
import session from 'express-session';
import passport from 'passport';
import mongoose from 'mongoose';

const __dirname = path.resolve();

const app = express();

const port = process.env.PORT || 5000;

//body parser
app.use(express.json());
app.use(express.urlencoded({extended: false}));

mongoose.Promise = global.Promise;

//Database Connection
mongoose.connect(
  'mongodb://localhost:27017/passport',
    {useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false }
)
    .then(() =>  console.log('Database Connected'))
    .catch(error => console.log(error));

//Session
app.use(session({
    secret: 'secret',
    resave: true,
    saveUninitialized: true
}));

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

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

//Global Variables
app.use((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');
    res.locals.user = req.user || null;
    next();
});

//set view engine
app.use(expressLayouts);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')))


//load routes
import indexRouter from './routes/index.js';
import usersRouter from './routes/users.js';
app.use('/', indexRouter);
app.use('/users', usersRouter);



app.listen(port, ()=>{
    console.log(`Server running on port ${port}`);
});

Now let us explain the above code.

First, we import Express and create our Express app by calling express(). Then we define the directory from which to serve our static files.

As you can, see we’re configuring express-session with a secret to sign the session ID cookie (you should choose a unique value here), and two other fields, resave and saveUninitialized. The resave field forces the session to be saved back to the session store, and the saveUninitialized field forces a session that is “uninitialized” to be saved to the store. To learn more about them, check out their documentation, but for now it’s enough to know that for our case we want to keep them false.

Then, we import passport and initialize it along with its session authentication middleware, directly inside our Express app

Routes

Now let us create six directories controllers, middleware, models, public, routes, views. Inside routes directory, let's create two files index.js and users.js.

Inside ./routes/index.js, add this code:

import Router from 'express';
const router = Router();

//auth middleware
import auth from "../middleware/auth.js";

router.get('/', (req, res)=>{
    res.render('welcome');
});

//dashboard
router.get('/dashboard', auth, (req, res)=>{
    res.render('dashboard', {name: req.user.name});
});

export default router;

In the above code, we're making use of express router to handle our app routes. Obviously we have two routes. The first get request will render a view welcome which will display our app's landing page.

The second route will display the users dashboard which we will handle very soon. Then finally, we export it to use in ./index.js.

Let's add one more file that will handle users routes ./routes/users.js

import Router from 'express';
const router = Router();

import UserController from "../controllers/UserController.js";

//Login
router.get('/login', UserController.showLoginForm);
router.post('/login', UserController.login);

//Register
router.get('/register', UserController.registerationForm);
router.post('/register', UserController.register);

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

export default router;

Controllers

Now let's create a file ./controllers/usersController.js with this content

import Validator from 'validatorjs';
import bcrypt from 'bcryptjs';
import passport from 'passport';
import User from "../models/User.js";

import passportLocal from 'passport-local';
const LocalStrategy = passportLocal.Strategy;


//Passport auth
passport.use(
    new LocalStrategy({usernameField: 'email', }, (email, password, done) =>{
        //Match User
        User.findOne({email: email.toLowerCase()})
            .then(user => {
                if (!user){
                    return done(null, false, {message: 'Email not found'})
                }
                //Match Passwords
                bcrypt.compare(password, user.password, (err, isMatched) => {
                    if (err) throw err;

                    if (isMatched){
                        return done(null, user);
                    }else {
                        return done(null, false, {message: 'Incorrect Password'})
                    }
                });


            })
            .catch(err => console.log(err))

    })
);


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

passport.deserializeUser((id, done) => {
    User.findById(id, (err, user) => {
        done(err, user);
    });
});  // End of passport config



const UserController = {

    showLoginForm: (req, res) => {
        //If already logged in return to dashboard
        if (req.isAuthenticated()) {
            return res.redirect('/dashboard');
        }

        res.render('login');
    },

    login: (req, res, next) => {

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

    },

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

    registerationForm: (req, res) => {
        //If already logged in return to dashboard
        if (req.isAuthenticated()) {
            return res.redirect('/dashboard');
        }

        res.render('register');
    },

    register: async (req, res) => {

       //Validate requests
        const validation = new Validator(
            {
                name: req.body.name,
                email: req.body.email,
                password: req.body.password,
                password_confirmation: req.body.password_confirmation
            },
        {
            name: ['required', 'string', 'min:4', 'max:255'],
            email: ['required', 'email'],
            password: ['required', 'min:6', 'confirmed']
        }
        );
    // If validation fails

        if (validation.fails()){
            //get errors
            const errname = validation.errors.get('name');
            const erremail = validation.errors.get('email');
            const errpassword = validation.errors.get('password');
            //get form data
            let {name, email, password} = req.body;
           return  res.render('register', {errname, erremail, errpassword, name, email, password} );
        }

        //Validation pass
        //Check if email exists
        const emailExists = await User.findOne({email: req.body.email})

        if (emailExists) {
            const {name, email, password} = req.body;
            let erremail = 'This email already exists';
            return res.render('register', {name, email, password, erremail} );
        } else  //If Email does not exist...
        {

            //save new user
            await validation.passes(()=> {


                //Hash Password
                bcrypt.genSalt(10, function(err, salt) {
                    bcrypt.hash(req.body.password, salt, function(err, hash) {

                        const newUser = new User({
                            name: req.body.name,
                            email: req.body.email.toLowerCase(),
                            password: hash
                        });

                        newUser.save().then(() =>{
                            //Redirect to Login Page
                            req.flash('success_msg', 'Registration successful, you may login now')
                            return  res.redirect('login')
                        }).catch(() => {
                            return res.send('could not save user');
                        });

                    });
                }); // ./bcrypt

            }); // ./validation passes


        }


    } // ./register Async


}

export default UserController;

Inside the above code we use:

  • Validator.js: To validate our form inputs before it hits the database.

  • Bcrypt.js: For hashing our passwords before it gets to the database.

  • Passport: authentication middleware for Node. js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application.

  • Passsport-local: To handle email and passwords.

And of course we are importing ../models/User.js. Which we will explain as we move on.

Models

Inside ./models/User.js, let's have this code where we define our schema with the help of mogoose schema

import mongoose from 'mongoose';
const Schema = mongoose.Schema;

const UserSchema = new Schema({
    name:{
        type: String,
        required: true,
    },

    email:{
        type: String,
        required: true,
        unique: true
    },

    password: {
        type: String,
        required: true
    },

    date: {
        type: Date,
        default: Date.now()
    }
});

const User = mongoose.model('users', UserSchema);
export default User;

Middleware

Now inside ./midleware/auth.js, we will have a code to make sure it the user is not logged in, he'll be redirected login page.

// ./midleware/auth.js
const auth = (req, res, next) => {
    if (req.isAuthenticated()){
       return  next();
    }
    req.flash('error_msg', 'Please login');
    res.redirect('/users/login');
}

export default auth;

public

This directory that will serve our static files. Inside ./public/ we'll have two directories css and js where we can add bootstrap.css and bootstrap.js files respectively . Alternatively, bootstrap cdn can be found here

Views

The views directory is where our html files can be found. We'll begin by creating one directory ./views/layout and five files ./views/welcome.ejs, ./views/dashboard.ejs

./views/layout is going to have two filese ./views/layout/header.ejs and ./views/layout/footer.ejs.

EJS is a simple templating language that lets you generate HTML markup with plain JavaScript.


 // ./views/layout/header.ejs

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Nodejs Passport Authentication</title>
    <link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body style="background: #ccc;">

<nav class="navbar navbar-expand-lg navbar-dark bg-info">
    <a class="navbar-brand" href="/">Passport Authentication</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse text-center" id="navbarNav">
        <ul class="navbar-nav">

            <% if (user == null){ %>

                <li class="nav-item">
                    <a class="nav-link" href="/users/login">Login</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/users/register">Register</a>
                </li>

            <%  } %>


            <li class="nav-item">
                <a class="nav-link" href="/dashboard"><b>Dashboard</b></a>
            </li>

        </ul>
    </div>
</nav>

<div class="container">
    <div class="row justify-content-center">
        <div class="col col-md-4">
// ./views/layout/footer.ejs

        </div>
    </div>
</div>
<script src="/js/jquery-3.3.1.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script>

</script>
</body>
</html>
// ./views/welcome.ejs
<%- include('layouts/header') -%>
<h1>Welcome to passport authentication</h1>
<%- include('layouts/footer') -%>
// ./views/dashboard.ejs
<%- include('layouts/header') -%>

<h1>Dashboard</h1>
    <h3>
        Welcome
        <span class="text-success font-weight-bold"><%= name %></span>
    </h3>
<a href="/users/logout">Logout</a>

<%- include('layouts/footer') -%>
// ./views/login.ejs

<%- include('layouts/header') -%>

<h1 class="text-info text-center mt-4">Login</h1>

<% if (success_msg != ''){ %>
    <div class="alert alert-success alert-dismissible fade show" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
        <%= success_msg %>
    </div>
<% } %>


<% if (error_msg != ''){ %>
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
        <%= error_msg %>
    </div>
<% } %>


<% if (error != ''){ %>
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
        <%= error %>
    </div>
<% } %>

<form action="/users/login" method="post">


    <div class="form-group">
        <input type="email" name="email" placeholder="Email" class="form-control">
    </div>

    <div class="form-group">
        <input type="password" name="password" placeholder="Password" class="form-control">
    </div>


    <div class="form-group">
        <input type="submit" placeholder="Confirm Password" value="Login" class="btn btn-primary btn-block">
    </div>

</form>

<%- include('layouts/footer') -%>
// ./views/register.ejs
<%- include('layouts/header') -%>

<h1 class="text-info text-center mt-4">Register</h1>

<form action="/users/register" method="post">

  <!-- Name -->
    <div class="form-group">
        <input type="text" name="name"
               placeholder="Name"
               class="form-control <%= typeof errname !== 'undefined'? 'is-invalid': '' %>"
               autocomplete="off"
               value="<%= typeof name !=='undefined' ? name : ''%>"
        >

    <!--  Name Errors     -->
        <% if (typeof errname !== 'undefined') { %>

            <span class="invalid-feedback error-message" role="alert">
               <strong><%= errname %></strong>
            </span>

        <%  } %>

    </div>

 <!-- Email -->
    <div class="form-group">
        <input type="email" name="email"
               placeholder="Email"
               class="form-control <%= typeof erremail !== 'undefined'? 'is-invalid': '' %>"
               value="<%= typeof email !== 'undefined' ? email : ''%>"
        >

        <!--  Email Errors      -->
        <% if (typeof erremail !== 'undefined'){ %>
            <span class="invalid-feedback error-message" role="alert">
               <strong><%= erremail %></strong>
            </span>
        <% } %>

    </div>

<!-- Password -->
    <div class="form-group">
        <input type="password"
               name="password"
               placeholder="Password"
               class="form-control <%= typeof errpassword !== 'undefined'? 'is-invalid': '' %>"
         value="<%= typeof password !== 'undefined' ? password : ''%>"
        >

        <!--   Password Errors     -->
        <% if (typeof errpassword !== 'undefined'){ %>

            <span class="invalid-feedback error-message" role="alert">
               <strong><%= errpassword %></strong>
            </span>

        <% } %>
    </div>

  <!--   Confirm Password     -->
    <div class="form-group">
        <input type="password"
               name="password_confirmation"
               placeholder="Confirm Password"
               class="form-control"
        >
    </div>

    <div class="form-group">
        <input type="submit" placeholder="Confirm Password" value="Submit" class="btn btn-primary btn-block">
    </div>

</form>

<%- include('layouts/footer') -%>

Now we can start our app by typing node index.js from the terminal