Day Log App in Node.js Part 8: Add user authentication

I wanted to learn how to implement user authentication in a Node application. In our first Laravel implementation, it was an easy task because this feature is already provided at the start with just a command. However Node requires external libraries for such functionality.

Fortunately I found Daniel Gynn’s easy-to-follow tutorial on how to integrate the Passport library into our current application.

Table of Contents

Steps

The following steps and implementation were mostly taken from Gynn’s original article.

Install dependencies

The following packages are needed:

Install them with the command:

npm install passport passport-local bcrypt-nodejs connect-flash express-session --save

Setup dependencies

The packages are setup in app.js:

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var flash = require('connect-flash');
var session = require('express-session');
...
app.use(session({ secret: 'shhsecret' }));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());

User model

Create the file model/user.model.js with just the username and password fields.

'use strict';

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

var UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    }
});

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

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

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

The methods generateHash and validPassword will be used later for the authentication process.

Routes

Create the routes/user.route.js file:

'use strict';

var express = require('express');
var router = express.Router();
var database = require('../config/database');

var UserController = require('../controller/user.controller');

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

router.get('/register', UserController.showRegister);

router.post('/register', UserController.register);

router.get('/login', UserController.showLogin);

router.post('/login', UserController.login);

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

module.exports = router;

This will manage the routes to login, registration and logout and their implementations are in the controller file.

Controller

Create the file controller/user.controller.js:

'use strict';

var passport = require('passport');

var UserModel = require('../model/user.model');
var constants = require('../config/constants');

const LOGIN_URL = "/user/login";
const REGISTER_URL = "/user/register";
const FORM_VIEW = "user/form";

var UserController = {
    index: (request, response) => {
        response.render('index');
    },

    showRegister: (request, response) => {
        if(request.isAuthenticated()) {
            response.redirect('/');
        } else {
            response.render(FORM_VIEW, {
                message: request.flash('error-register'),
                type: "alert-danger",
                operation: "Register",
                action: REGISTER_URL
            });
        }
    },

    register: passport.authenticate('register', {
        successRedirect: '/',
        failureRedirect: REGISTER_URL,
        operation: "Register",
        action: REGISTER_URL,
        failureFlash: true,
    }),

    showLogin: (request, response) => {
        if(request.isAuthenticated()) {
            response.redirect('/');
        } else {
            response.render(FORM_VIEW, {
                message: request.flash('error-login'),
                type: "alert-danger",
                operation: "Login",
                action: LOGIN_URL
            });
        }
    },

    login: passport.authenticate('login', {
        successRedirect: '/',
        failureRedirect: LOGIN_URL,
        failureFlash: true,
    }),

    logout: (request, response) => {
        request.logout();
        response.redirect('/');
    }
};

module.exports = UserController;

The index method points to the application’s main page.

showRegister and showLogin shows the form with their respective attributes. register and login uses Passport’s authenticate method to handle the respective logic specified by the first parameters (namely “register” and “login”) which are defined in config/passport.js.

Logout simply logs out the user and returns to the index page.

Form page

Both the registration and login uses the same form with different attributes to differentiate the two.

<% include ../partials/header %>

<div class="card">
    <h3 class="card-header"><%= operation %></h3>

    <div class="card-block">

        <form action="<%= action %>" method="post">

            <% if (typeof message !== 'undefined') { %>
                <% if (message.length) { %>
                    <%- include('../partials/alert', { message: message, type: type }) %>
            <% }
            } %>

            <div class="container">
                <div class="form-group row">
                    <label for="username" class="col-sm-2 col-form-label text-right">Username</label>
                    <div class="col-sm-10">
                        <input type="text" class="form-control" id="username" name="username"
                            placeholder="Enter your username" required>
                    </div>
                </div>

                <div class="form-group row">
                    <label for="password" class="col-sm-2 col-form-label text-right">Password</label>
                    <div class="col-sm-10">
                        <input type="password" class="form-control" id="password" name="password"
                            placeholder="Enter your password" required>
                    </div>
                </div>

                <div class="form-group row">
                    <div class="offset-sm-2 col-sm-10">
                        <button type="submit" class="btn btn-primary">Login</button>
                        <button type="reset" class="btn btn-danger">Reset</button>
                    </div>
                </div>
            </div><!-- .container -->

        </form>

    </div><!-- .card-block -->

    <div class="card-footer text-center">
        <a href="/" class=" btn btn-primary btn-sm">Back</a>
    </div>

</div><!-- .card -->


<% include ../partials/footer %>

Login page Register page

Authentication messages are shown when there is a failure that requires modification in partials/alert.ejs:

<% if(typeof errors !== "undefined") { %>
    <%= errors.find(error => error.param === field).msg %>
<% } else { %>
    <%= message %>
<% } %>

Header options

I also made changes to the header file to show the user options of “Login”, “Registration”, and “Logout” depending on the session.

<nav class="navbar navbar-toggleable-md navbar-light bg-faded"
    style="margin-bottom: 2em">
    <div class="container col-sm-6">
        <button class="navbar-toggler navbar-toggler-right" type="button"
            data-toggle="collapse" data-target="#navbar"
            aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <a class="navbar-brand" href="/">DayLogApp</a>

        <div class="collapse navbar-collapse" id="navbar">
            <ul class="nav navbar-nav ml-auto w-100 justify-content-end">
                <% if(typeof loggedIn == 'undefined') { %>
                    <li class="nav-item">
                        <a class="nav-link" href="/user/login">Login</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/user/register">Register</a>
                    </li>
                <% } else if(loggedIn) { %>
                    <li class="nav-item">
                        <a class="nav-link" href="/user/logout">Logout</a>
                    </li>
                <% } %>
            </ul>
        </div>
    </div>
</nav>

Header register and login Header logout

Passport configuration

Create the file config/passport.js:

var LocalStrategy = require('passport-local').Strategy;
var UserModel = require('../model/user.model');

const localStrategy = {
    usernameField: 'username',
    passwordField: 'password',
    passReqToCallback: true,
};

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

    passport.deserializeUser((id, done) => {
        UserModel.findById(id, (error, user) => {
            done(error, user);
        });
    });

    passport.use('register',
        new LocalStrategy(localStrategy,
            (request, username, password, done) => {
                process.nextTick(() => {
                    UserModel.findOne({ username:  username },
                        (error, user) => {
                            if (error) return done(error);

                            if (user) {
                                return done(null, false,
                                    request.flash('error-register',
                                        'Username "' + username + '" is already in use.'));
                            } else {
                                var newUser = new UserModel();
                                newUser.username = username;
                                newUser.password = newUser.generateHash(password);
                                newUser.save((error) => {
                                    if (error) throw error;

                                    return done(null, newUser);
                                });
                            }
                        }
                    );
                });
        }
    ));

    passport.use('login',
        new LocalStrategy(localStrategy,
            (request, username, password, done) => {
                UserModel.findOne({ username:  username },
                    (error, user) => {
                        if (error) throw error;

                        if (!user)  {
                            return done(null, false,
                                request.flash('error-login',
                                    'No username "' + username + '" found.'));
                        }

                        if (!user.validPassword(password)) {
                            return done(null, false,
                                request.flash('error-login',
                                    'Wrong username/password combination.'));
                        }

                        return done(null, user);
                    }
                );
            }
        )
    );
};

The Passport use declarations “register” and “login” correspond to the same names in the controller with the authenticate method. Inside each are flash messages that are also called in the controller.

Add this in app.js as a dependency to use:

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

User page restrictions

The DayLog page is only accessible to logged-in users so a few changes are reguired.

In routes/index.route.js, we set the variable loggedIn to true if the user is authenticated:

router.use((req, res, next) => {
    if (req.isAuthenticated()) res.locals.loggedIn = true;

    return next();
});

This loggedIn variable is the one checked in the header view file to choose which user option to show.

Next, we add a new method in controller/daylog.controller.js to redirect all un-logged-in users that want to access the /daylog/* route. Here are the changes in route/daylog.route.js and the controller respecitvely:

router.use(DayLogController.isUserLoggedIn);
isUserLoggedIn: (request, response, next) => {
    if (!request.isAuthenticated()) {
        response.redirect('/user/login');
    } else {
        return next();
    }
},

Problems

I encountered about an interesting problem during development. I made the methods in the Passport configuration file to use the arrow signature:

UserSchema.methods.validPassword = (password) => {
    return bcrypt.compareSync(password, this.password);
};

When I tried to login with OK details (username and password present in database) (I found no problems when the credentials are invalid), the application stopped with the following error log:

events.js:165
      throw err;
      ^

Error: Uncaught, unspecified "error" event. (Incorrect arguments)
    at Function.emit (events.js:163:17)
    at Query.<anonymous> (/path/to/daylogapp-node/node_modules/mongoose/lib/model.js:3739:13)
    at /path/to/daylogapp-node/node_modules/kareem/index.js:277:21
    at /path/to/daylogapp-node/node_modules/kareem/index.js:131:16
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
    at process._tickCallback (internal/process/next_tick.js:98:9)

npm ERR! Darwin 16.5.0
npm ERR! argv "/usr/local/bin/node" "/usr/local/bin/npm" "start"
npm ERR! node v6.4.0
npm ERR! npm  v3.10.3
npm ERR! code ELIFECYCLE
npm ERR! daylogapp-node@0.0.0 start: `node ./bin/www`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the daylogapp-node@0.0.0 start script 'node ./bin/www'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the daylogapp-node package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     node ./bin/www
npm ERR! You can get information on how to open an issue for this project with:
npm ERR!     npm bugs daylogapp-node
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls daylogapp-node
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     /path/to/daylogapp-node/npm-debug.log

I learned that accessing this in arrow-style methods is the cause of this.

References

Twitter, LinkedIn