Making a full-stack web application can be challenging, even when using tools like Node (which runs server-side javascript code on a command line like environment) and some of the available installable packages. Thus, this article is written to help developers write applications with user authentication. In the spirit of providing valuable information readers, we’ll focus on user authentication for full-stack javascript applications using node and two of its packages. Here we will discuss the usage of “Passport” and “bcrypt” packages as middleware for programming user authentication using Node.js.

The authentication middleware which we’ll examine is the Passport package since it works well in Node.js “Express” package based applications, allowing users to log in with username and password, or other third party verifications, such as Facebook. In this article, we’ll examine the usage of passport for username and password verification with a self-generated JSON Web Token. Passport can generate, extract, and validate these web tokens with an expiration date and data for checking users when choosing local authentication strategy. The password hashing middleware which we use to compliment Passport’s functionality is the Bcrypt package. This tool allows us to save the user in the database to later compare with the password (which should be encrypted) used when generating their authentication token. Make sure to include all the following packages in your main server file: “express”, “body-parser”, “express-handlebars”, “passport”, “connect-flash”, “cookie-parser”, and “express-session” while passing passport for your configuration file as shown in the figure 1.

require("dotenv").config();
var express = require("express");
var bodyParser = require("body-parser");
var exphbs = require("express-handlebars");
var passport     = require('passport');
var flash        = require('connect-flash');
var cookieParser = require('cookie-parser');
var session      = require('express-session'); 

var app = express();
var PORT = process.env.PORT || 8080;

var db = require("./models");

Figure 1.

All the packages displayed in figure one should be installed using “npm install” from the command line before running the server. It’s best practice to include a “.env” file in your project to hold all the users, passwords, and keys required to run the packages for the desired application. If a “.env” is not included in your project then your server should include code that looks very similar to the one displayed in figure two. The code in figure two displays the configuration information for the passport-crypt authentication where the variable labeled as secret is a randomly generated hash uniquely for each project with a length of at least 25 characters, and the variable labeled as key is the id which we will be checking for with a set-cookie and reserve and saveUnitialized preferences expressed as boolean values.

app.use(bodyParser.urlencoded({extended:true}));
app.use(bodyParser.json());


app.engine("handlebars", exphbs({defaultLayout: "main"}));
app.set("view engine","handlebars");

app.use(express.static("public"));

app.use(session({
    key: 'user_sid',
    secret: 'goN6DJJC6E287cC77kkdYuNuAyWnz7Q3iZj8',
    resave: false,
    saveUninitialized: false,
    cookie: {
        expires: 600000
    }
}));

app.use(passport.initialize());
app.use(passport.session()); 
app.use(flash());

require("./controllers/html-routes")(app, passport);
require("./controllers/account-controller")(app, passport);
require("./controllers/item-controller")(app, passport);
require("./controllers/search-controller")(app, passport);
require("./controllers/transactions-controller")(app, passport);


db.sequelize.sync().then(function(){
    app.listen(PORT, function(){
        console.log("Listening on localhost:" + PORT);
    })
})

Figure 2.

After making the suggested adjustments to our server file we should create a js file within our config directory to set up our passport connection. This file should include the type of strategy which passport will follow as well as the existing models to interact with and a module export setup for such function (which will check for our user-generated id with the label of “uuid” in this case) as displayed in figure 3.

var LocalStrategy = require('passport-local').Strategy;

var db  = require('../models');

module.exports = function(passport) {
    passport.serializeUser(function(user, done) {
        done(null, user.uuid);
    });

    passport.deserializeUser(function(uuid, done) {
        db.Accounts.findById(uuid).then(function(user) {
	        if (user) {
	            done(null, user.get());
	        } else {
	            done(user.errors, null);
	        }
	    });
    });

Figure 3.

After such setup, we should set up passport for when creating an account and using local strategy user authentication. In this case, we will be creating an account with a username equivalent to the user’s email and a password field which has been labeled as “account_key” in figure 4.

passport.use('local-signup', new LocalStrategy({
        usernameField: 'email',
        passwordField : 'account_key',
        passReqToCallback : true
    },

Figure 4.

As part of the same function, we will pass a call back function as displayed in figure 5 that will save and associate the created user with their email and handle potential errors when doing so.

function(req, email, account_key, done) {
        process.nextTick(function() {
        db.Accounts.findOne({
            where: {
            	email: email
            }
        }).then(function(user, err){
        	if(err) {
                console.log("err",err)
                return done(err);
            } 
            if (user) {
            	console.log('signupMessage', 'That email is already taken.');
                return done(null, false, req.flash('signupMessage', 'That email is already taken.'));
            } else {
                db.Accounts.create({
                            first_name:req.body.first_name,
                            last_name:req.body.last_name,
                            street: req.body.street,
                            city: req.body.city,
                            state: req.body.state,
                            zip: req.body.zip,
                            balance: req.body.balance,
			    email: req.body.email,
                            phone: req.body.phone,
			    account_key: db.Accounts.generateHash(account_key)

						    }).then(function(dbUser) {
						    	
						      
	return done(null, dbUser);

}).catch(function (err) { console.log(err);}); 
            }
          });   
        });
}));

Figure 5.

As displayed in figure 5, if there are no errors in the creation of such account then we will be sending the information back to render such data and catch any errors if there are any.

Just like we had a set up for when creating an account, we should have one for when users log in to their account as seen in figure 6 where the username is again the email and the password the account_key variable, just like when we created the user account.

passport.use('local-login', new LocalStrategy({
        usernameField: 'email',
        passwordField : 'account_key',
        passReqToCallback : true 
    },

Figure 6.

In a similar fashion as when we created an account for a unique email, we will find an account directly linked to such email here. If the account is found and the password is correct then we will render such information and allow a user to use further functionality. If no user is found, or the password is wrong we’ll return the respective errors as seen in the code displayed in figure 7.

function(req, email, account_key, done) { 
        db.Accounts.findOne({
            where: {
                email: req.body.email 
            }
        }).then(function(user, err) {
            (!user.validPassword(req.body.account_key)));
            if (!user){
                console.log("no user found");
                return done(null, false, req.flash('loginMessage', 'No user found.')); 
            }
            if (user && !user.validPassword(req.body.account_key)){
            return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); 
            }
            return done(null, user);
        });
    }));

};

Figure 7.

Once we are done with the configuring our server and our main set up for using passport we should proceed to adapt our HTML route to include checking for user authentication. As we can see in figure 8 we call a function to check for user authentication when sending the request before rendering pages which require user information.

module.exports = function(app){
    app.get("/", function(req,res){
        if(req.isAuthenticated()){
            var user = {
                id: req.session.passport.user,
                isloggedin: req.isAuthenticated()
            }
            res.render("home", user);
        }
        else{
            res.render("home");
        }
    })

    app.get("/list-items", function(req,res){
        res.render("search");
    });

    app.get("/signup", function(req,res){
        if(req.isAuthenticated()){
            res.redirect("/acounts/view");
        }else{
           res.render("accounts"); 
        }
    });

    app.get("/add-items", function(req, res){
        if(req.isAuthenticated()){
            res.render("add-items");
        }else {
            res.redirect()
        }
    })
};

Figure 8.

It is important to take into account the implementation of passport into all other route files such as controllers, as well as onto your js account file. Note that we should require crypt and the id we are checking for in each of the models to be able to check for authentication as shown in figure 9.

var uuidv1  = require('uuid/v1');
var bcrypt  = require('bcrypt');

module.exports = function(sequelize, DataTypes) {
    var Accounts = sequelize.define("Accounts", {
        uuid: {
          primaryKey: true,
          type: DataTypes.UUID,
          defaultValue: DataTypes.UUIDV1,
          isUnique :true
        },
        first_name: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [1, 30]
            }
        },
        last_name: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [1, 30]
            }
        },
        street: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [1, 30]
            }
        },
        city: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [1, 30]
            }
        },
        state: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [1, 2]
            }
        },
        zip: {
            type: DataTypes.INTEGER,
            allowNull: false,
            validate: {
                len: [5]
            }
        },
        balance: {
            type: DataTypes.DECIMAL(12, 2),
            defaultValue: 0
        },
        email: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [1, 100]
            }
        },
        phone: {
            type: DataTypes.STRING,
            allowNull: false,
            validate: {
                len: [10]
            }
        },
        account_key: {
            type: DataTypes.STRING,
            required: true,
            validate: {
                len:[8]
            }
        }

    });

Figure 9.

Make sure to include a hash generator and password verification methods within your model using crypt hashing and compareSync respectively as displayed in figure 10.

// methods ======================
      // generating a hash
      Accounts.generateHash = function(password) {
          return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
      };

      // checking if password is valid
      Accounts.prototype.validPassword = function(password) {
          return bcrypt.compareSync(password, this.account_key);
      };

    Accounts.associate = function(models){
        Accounts.hasMany(models.Items, {
            foreignKey: "owner_id",
            onDelete: "cascade"
        });
    };

    Accounts.associate = function(models){
        Accounts.hasMany(models.Transactions, {
            foreignKey: "renter_id"
        });
    };

    return Accounts;
}

Figure 10.

We should incorporate passport into all of our other routes as we did into our HTML routes and models. Here we will examine the controller which routes the required paths for the account model which we previously examined. In such a file, we should require passport and incorporate our isAuthenticated() function where we target a specific id before rendering the desired information and allow users to have such functionality. Such code is displayed in figure 11.

var db = require("../models");

var passport = require('passport');

module.exports = function (app) {

    app.get("/signup", function (req, res) {
        res.render("accounts");
    });

    app.get("/accounts/view", function (req, res) {
        console.log("%%%%%%%%% is logged in", req.isAuthenticated());
       
        if(req.isAuthenticated()){
         
          db.Accounts.findOne({
            where:{
              uuid: req.session.passport.user
            }
          }).then(function(dbUser){
            var user = {
              userInfo: dbUser.dataValues,
              id: req.session.passport.user,
              isloggedin: req.isAuthenticated()
            }
            res.render("view-account", user);
          })
        }
        else {
          var user = {
              id: null,
              isloggedin: req.isAuthenticated()
            }
          res.redirect("/");
        }
    });

Figure 11.

Keep in mind that the sign-up form and all other functionality should follow the same patterns that have been displayed in the code included in this article to ensure functionality. In order to keep track of the user login and logout, we should implement the use of cookies as we foreshadowed in our server initial configuration. When the user logs out or the session is finished then the cookie should be cleared and the user redirected to home. The user’s information should be stored in such cookie respectively when login in. This is displayed in figure 12.

	    app.get('/logout', function(req, res) {
        req.session.destroy(function(err){
          req.logout();
          res.clearCookie('user_sid');
          res.clearCookie('first_name');
          res.clearCookie('user_id');
          res.redirect('/');
        })
    });
  app.post('/signup', function(req, res, next) {
    passport.authenticate('local-signup', function(err, user, info) {
      console.log("info", info);
      if (err) {
        console.log("passport err", err);
        return next(err); // will generate a 500 error
      }
      if (! user) {
        console.log("user error", user);
        return res.send({ success : false, message : 'authentication failed' });
      }
      req.login(user, loginErr => {
        if (loginErr) {
          console.log("loginerr", loginerr)
          return next(loginErr);
        }
        console.log('redirecting....');
        res.cookie('first_name', user.first_name);
        res.cookie('user_id', user.uuid );
        return res.redirect("/accounts/view");
      });      
    })(req, res, next);
  });

  app.post('/login', function(req, res, next) {
    passport.authenticate('local-login', function(err, user, info) {
      console.log("\n\n\n########userrrr", user)
      if (err) {
        console.log("passport err", err);
        return next(err); // will generate a 500 error
      }
      if (!user) {

        return res.send({ success : false, message : 'authentication failed'});
      }
      req.login(user, loginErr => {
        if (loginErr) {
          console.log("loginerr", loginErr)
          return next(loginErr);
        }
 
        console.log('redirecting....')
        res.cookie('first_name', user.first_name);
        res.cookie('user_id', user.uuid );

        return res.json(true);
        
      });      
    })(req, res, next);
  });
}

Figure 12.

Make sure to continuously test using console logs and include error handling functions when writing the code with the pattern described in this article. Using passport and bcrypt can be tough, but hopefully, this article will help you clear up any lingering doubts while implementing them into your code.

Find this code in Github: https://github.com/b0bbybaldi/Rent-All

Please follow up with any feedback or doubts about this article, thanks.

#nodejs

Nodejs Full Stack App for User Authentication
2 Likes36.40 GEEK