Local Email Authentication Using Node, Express and Passport For Production With CSRF Protection

Follow @desiprogrammer_

This blog has the code snippet about local authentication in Express JS using Passport JS . With the help of this we can authenticate using an email-password combination or a username-password combination.

This is not an in-depth explanation, rather it's just like a code snippet. For more information you can refer to the video tutorial given below 😊.

Project Setup

Setting up our project is fairly simple. I will use npm to initialize a project and will further install express for our express app , ejs for templating and mongoose for connecting to our mongoDB database.


 npm init --yes
 npm i express ejs mongoose
                    

Then I will create seperate directories (folders) named config, controllers, modals and views. I am using command line to create the folders, but feel free to use GUI as per your convenience.


 mkdir config controller modals views
                    

Having done that, I will simply copy my pre-coded Templates in the views directory and will replace the html extension with ejs.

Then I will first setup a basic Express App listening to PORT 8000, with ejs as a Templating Engine, in my index.js file in the root project Directory.


 const express = require('express');
 
 const app = express();

 app.set('view engine', 'ejs');
 app.set('views', __dirname + '/views',);

 const PORT = process.env.PORT || 8000;

 app.listen(PORT, () => 
    console.log("Server Started At " + PORT));
                    

I will then create a mongoKEY.js file in my config directory and will add my mongo URI there. You can also use dot files and remember to change this setup based on your Database Usage.


 module.exports = "YOUR MONGO URI HERE";
                    

And I will use this URI to connect to my mongo Db Cloud.


 // import mongoose 
 const mongoose = require('mongoose');

 const mongoURI = require('./config/monkoKEY');
 mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true, 
    useFindAndModify: false, useCreateIndex: true, 
    },).then(() => console.log("Connected !"),);
                    

I will then create a routes.jsfile in my controllers directory and this will contain all the basic routes for the app. Also make sure that you reference this file in index.js.

In index.js


 app.use(require('./controller/routes.js'));
                    

In controller/routes.js


 const express = require('express');
 const router = express.Router();
 
 
 router.get('/', (req, res) => {
     res.render("index");
 });
 
 router.get('/login', (req, res) => {
     res.render("login");
 });
 
 router.get('/signup', (req, res) => {
     res.render("signup");
 });
 
 router.get('/profile', (req, res) => {
     res.render("index");
 });
 
 module.exports = router;
                    

Adding CSRF Protection

What is CSRF and why I need to add CSRF protection ?

Well, Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated.
There are other methods to prevent CSRF attacks, but using a CSRF Token is the most common one. You can learn more about it Here.

Now let's move forward and set up CSRF Token Authentication and for that I have to first install csurf package that will help me in generating and verifying CSRF tokens, express-session for Handling Sessions and cookie-parser for Parsing Cookies.


 npm i csurf express-session cookie-parser
                    

Having Done that I will setup express-session and csurf, but remember that right now I will do only the bare-minimum setup of express-session and will talk more about it while using passport-js authentication.

In index.js and also follow the order.

 
 // parsing requests Body
 app.use(express.urlencoded({ extended: true }));
 
 // setting up sessions
 app.use(expressSession({
  secret: 'random',
 }));

 // using CSRF for entire app 
 // you can modify it to be used only specific routes
 // use Middlewares
 app.use(csrf());

 // app.use(require('./controller/routes.js'));
                    

And then I can use req.csrfToken() to generate a token and use this in my form as shown below.


 // While rendering a file
 res.render('signup', { err : err , csrfToken: req.csrfToken() },);
                    

  // Using it in a form
  <input type="hidden" name="_csrf" value= <%=csrfToken %> />
                    

Adding Our User Model

I will create a user.js file in our model directory and will add our user Schema.


 const mongoose = require('mongoose');

 const userSchema = new mongoose.Schema({
     username : {
         type : String,
         required : true,
     },
     email : {
         type : String,
         required : true,
     },
     password : {
         type : String,
     },
     // optional for verifying users
     isVerified : {
         type : Boolean,
         default : false,
     }
     // as I will add google authentication to same application later 
     // this will help with the understanding of handling multiple-auth providers 
     googleId : {
         type : String,
     },
     // this will store the auth provider
     provider : {
         type : String,
         required : true,
     },
 })

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

Signing Up A User

Now that I am done with setting up CSRF protection, I will handle user registration i.e Signup and will later see how to login these users.

As u can see in our signup.ejs file , I have a Form that submits to /submit route with POST method. So I need to create a POST route of signup and will handle further validations and user Creation there.

But I just need to add a couple of configurations for our signup process too.
First of all I need to install bcryptjs to hash our users password.


 npm i bcryptjs
    

 // in controller/routes.js file
        
 const bcrypt = require('bcryptjs');
 const user = require('../model/user');

 router.post('/signup', (req, res) => {
     // using destructuring to fetch all data from req.body
     const { username, email, password, confirmpassword } = req.body;
     if (!username || !email || !password || !confirmpassword) {
         // if any field is empty
         // render the page with error message and a new CSRF Token
         err = "Please Fill in all the fields...";
         res.render('signup', { err: err, csrfToken: req.csrfToken() });
     } else {
         // Skipping extra validation check for email and password strength
         // using the user model to check if a user exists with either the same email or username
         user.findOne({ $or: [{ email: email }, { username: username }] }, function (err, data) {
             if (err) throw err;
             if (data) {
                 // this shows a user exist either with the same email or username
                 // I will render the page again with error message and new csrfToken
                 err = "Account Exists..! Try Logging In";
                 res.render('signup', { err: err, email: email, csrfToken: req.csrfToken() });
             } else {
                 // no user exists
                 // create a user
                 // use bcrypt to generate a salt 
                 bcrypt.genSalt(12, (err, salt) => {
                     if (err) throw err;
                     // hashing the password with this salt 
                     bcrypt.hash(password, salt, (err, hash) => {
                         // creating a saving a new user to our database
                         user({
                             username: username,
                             email: email,
                             password: hash,
                             // joined: new Date(),
                             googleId: null,
                             provider: 'email',
                         }).save((err, data) => {
                             if (err) throw err;
                             // redirecting to home page
                             // you can change according to your will
                             // later I will replace this to automatically login a user after signing up
                             res.redirect('/');
                         });
                     });
                 });
             }
         });
     }
 });
    

Setting Up Passport Js

Now that I have signed up a user, I need to setup passport-js for logging in a user and creating a session for authorization.
I will first install passport and passport-local packages along with connect-flash package to show passport error and make sure that you already have cookie-parser installed from above step !


 npm i passport passport-local connect-flash
   

Then I will make changes to our index.js file for setting up express-session, cookie-parser, connect-flash and passport.


 // in index.js file 

 const cookieParser = require('cookie-parser');
 const expressSession = require('express-session');
 const passport = require('passport');
 const flash = require('connect-flash');

 // set up cookie parser
 // make sure that the secret from express-session is same Here
 // also the docs mention that express-session itself parses the cookie too, 
 // But there were some error , So I have set this here
 app.use(cookieParser('random'));

 // we will set up express-session
 app.use(expressSession({
    // This is the secret used to sign the session ID cookie.
    secret: 'random',
    // Forces the session to be saved back to the session store
    resave: true,
    // forces a new but non modified session to be saved
    saveUninitialized: false,
    // as secure can be used only with https
    // cookie: { secure: false },
    // make sure that session doesn't end on server refresh
    maxAge: 24 * 60 * 60 * 1000,
    // The default MemoryStore is ideal only for development and test
    // watch video for more explanation on this topic
    // store : new MemoryStore(),
  }));

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

 app.use(function (req, res, next) {
   // to show error from Passport flash
   // you can set up your own flash
   res.locals.error = req.flash('error');
   next();
 });
   

Now That our setup is done, I will make a new file named passportLocal.js in our controller directory, and will add the following code, thats exports a function which will be called from our login route and this function creates a localStrategy , finds the user from the database , compares the passwords and then either logs in the user and creates a session or shows the error message in our UI !


 // in controller/passportLocal.js 
 var localStrategy = require('passport-local').Strategy;
 const user = require('../model/user');
 const bcrypt = require('bcryptjs');
 
 module.exports = function (passport) {
     passport.use(new localStrategy({ usernameField: 'email' }, (email, password, done) => {
         user.findOne({ email: email }, (err, data) => {
             if (err) throw err;
             if (!data) {
                 return done(null, false, { message: "User Doesn't Exists.." });
             }
             bcrypt.compare(password, data.password, (err, match) => {
                 if (err) {
                     return done(null, false);
                 }
                 if (!match) {
                     return done(null, false, { message: "Password Doesn't Match" });
                 }
                 if (match) {
                     return done(null, data);
                 }
             });
         });
     }));
     // setting id as cookie in user's browser
     passport.serializeUser(function (user, done) {
         done(null, user.id);
     });
     
     // getting id from the cookie 
     passport.deserializeUser(function (id, done) {
         user.findById(id, function (err, user) {
             done(err, user);
         });
     });
 }
   

To understand more about serializeUser and deserializeUser, watch the video tutorial or visit this Stackoverflow Answer.

Now I can call this from our routes.js file's login route to actually login in a user.


 // in controller/routes.js

 const passport = require('passport');
 require('./passportLocal')(passport);

 // add csrfToken to login's get Router
 // remember to add this as a hidden form-field just as we did in signup
 router.get('/login', (req, res) => {
    res.render("login", { csrfToken: req.csrfToken() });
 });

 router.post('/login' , (req, res, next) => {
    passport.authenticate('local', {
        failureRedirect: '/',
        successRedirect: '/',
        failureFlash: true,
    })(req, res, next);
 });
   

Now , make sure that you have set your login form to submit to this route with post method and also add a alert to show error.

User Logout

Now that I have handled secure user signup and user login it's time to make sure that user can logout successfully.
Although, there is a common problem that almost everyone faces with user logout , but I will handle that too !

First, I will simple create a logout route in our routes file that logs out the user and sends them back to '/' Route.


    // in controller/routes.js file 
 router.get('/logout', (req, res) => {
     req.logout();
     req.session.destroy( function ( err ) {
         if(err) throw err;
         req.session = null;
         res.redirect('/');
     });
 })

But although I am destroying the session and making it null, pressing the back button will still show you the cached data. To solve that, I have to simply set some extra headers to our res i.e response , while rendering any authenticated page. I have attached the code below, that can you either use as an app level middleware, or only for your router or for only specific routes.


 app.use((req, res, next) => {
     if(req.isAuthenticated()){
         res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
     }
     next();
 });

You May Also Like..

Node JS, Express Js, Google Authentication, OAuth 2.0

Extending this project with Google 0Auth.

Language Icon