Hackernoon logoNode Authentication using Passport.js by@sujan-chhetri

Node Authentication using Passport.js

Author profile picture

@sujan-chhetriSujan Chhetri

sujjjan.ml

While building any web app people get confused or feel difficulty in authentication process. Creating a registration form and sign in process is a hectic process if we don't follow proper method.
In this tutorial we will see how we can register and login using passport-local.

Setting Up Our Application:

To set up our base Node application, we'll need a few things. We'll set up our npm packages, node application, configuration files, models, and routes.

Application Structure:

  - app
    ------ models
    ---------- user.js  <!-- our user model -->
    ------ routes.js    <!-- all the routes for our application -->
    - config
    ------ auth.js      <!-- will hold all our client secret keys (facebook, twitter, google) -->
    ------ database.js  <!-- will hold our database connection settings -->
    ------ passport.js  <!-- configuring the strategies for passport -->
    - views
    ------ index.ejs    <!-- show our home page with login links -->
    ------ login.ejs    <!-- show our login form -->
    ------ signup.ejs   <!-- show our signup form -->
    ------ profile.ejs  <!-- after a user logs in, they will see their profile -->
    - package.json      <!-- handle our npm packages -->
    - server.js         <!-- setup our application -->
Create all those files and folders and we'll fill them in as we go along.

Packages / package.json:

We are going to install all the packages needed for the entire tutorial series. This means we'll install all the packages needed for passport local and the other things we need.
"dependencies": {
    "bcrypt-nodejs": "0.0.3",
    "body-parser": "^1.19.0",
    "connect-flash": "^0.1.1",
    "cookie-parser": "^1.4.5",
    "ejs": "^3.0.1",
    "express": "^4.17.1",
    "express-session": "^1.17.0",
    "method-override": "^3.0.0",
    "mongoose": "^5.9.6",
    "morgan": "^1.10.0",
    "passport": "^0.4.1",
    "passport-local": "^1.0.0",
    
  }
Most of the packages are self-explanatory.
  • Express  is the framework.
  • Ejs is the templating engine.
  • Mongoose is object modeling for our MongoDB database.
  • Passport stuff will help us authenticating with different methods.
  • Connect-flash allows for passing session flashdata messages.
  • Bcrypt-nodejs gives us the ability to hash the password.
npm i
 With all of our packages ready to go, let's set up our application in 
server.js
.

Setting up server.js:

Let's make all our packages work together nicely. Our goal is to set up this file and try to have it bootstrap our entire application. We'd like to not go back into this file if it can be helped. This file will be the glue for our entire application.
var express  = require('express');
var app      = express();
var port     = process.env.PORT || 8000;
var mongoose = require('mongoose');
var passport = require('passport');
var flash    = require('connect-flash');

var morgan       = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser   = require('body-parser');
var session      = require('express-session');

 // connect to our database
mongoose.connect('mongodb://localhost/blog', {
                    useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true
})
// pass passport for configuration

// set up our express application
app.use(morgan('dev')); // log every request to the console
app.use(cookieParser()); // read cookies (needed for auth)
app.use(bodyParser()); // get information from html forms

app.set('view engine', 'ejs'); // set up ejs

// required for passport
app.use(session({ secret: 'ilovenodejs' })); // session secret
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session

// routes 
require('./app/routes.js')(app, passport); 
require('./config/passport')(passport); 


app.listen(port);
The path of our passport object is important to note here. We will create it at the very beginning of the file with var passport = require('passport');. Then we pass it into our config/passport.js file for it to be configured. Then we pass it to the app/routes.js file for it to be used in our routes.
Now with this file, we have our application listening on port 8000. All we have to do to start up our server is:
npm i nodemon
"scripts": {
    "on": "nodemon server.js",
    "start": "node server.js"
  }
In json file edit as above.
To start the node server,In terminal:
nodemon
Our server will start.Read about nodemon here

Routes app/routes.js:

We will have the following routes:
  • Home Page (/)
  • Login Page (/login)
  • Signup Page (/signup)
  • Profile Page (/profile)
    module.exports = function(app, passport) {
    // =======================HOME-PAGE ===============================
     app.get('/', function(req, res) {
    res.render('index.ejs'); // load the index.ejs file
    });
    // ==========================LOGIN ===============================
    app.get('/login', function(req, res) {
    res.render('login.ejs', { message: req.flash('loginMessage') }); 
    });
    
    // =====================SIGNUP ==============================
    app.get('/signup', function(req, res) {
    res.render('signup.ejs', 
    { message: req.flash('signupMessage') });
    });
    
     //==========================profile =====================      
    app.get('/profile', isLoggedIn, function(req, res) {
    res.render('profile.ejs', {
    user : req.user
    });
    });
    // LOGOUT 
    app.get('/logout', function(req, res) {
    req.logout();
    res.redirect('/');
    });
    
    // process the signup form
    app.post('/signup', passport.authenticate('local-signup', {
    successRedirect : '/profile', //redirect to the secure profile section
    failureRedirect : '/signup', // redirect back to the signup page if there is an error
    failureFlash : true // allow flash messages
    }));
    
     // process the login form
    app.post('/login', passport.authenticate('local-login',{
    successRedirect : '/profile', //redirect to the secure profile section
    failureRedirect : '/login', // redirect back to the signup page if there is an error
    failureFlash : true // allow flash messages
    }));
    
    };
    
     // makes sure a user is logged in
    function isLoggedIn(req, res, next){
    
    // if user is authenticated in the session, carry on 
    if (req.isAuthenticated())
    return next();
    
    // if they aren't redirect them to the home page
    res.redirect('/');
    }
    This is our app/routes.js file.

    Home Page views/index.ejs:

    Our home page will just show links to all our forms of authentication.
    <!doctype html>
    <html>
    <head>
    <title>Authentication</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> <!-- load bootstrap css -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css"> <!-- load fontawesome -->
    <style>
    body        { padding-top:80px; }
    </style>
    </head>
    <body>
    <div class="container">
    
    <div class="jumbotron text-center">
    <p>Login or Register with:</p>
    <a href="/login" class="btn btn-default"><span class="fa fa-user"></span> Local Login</a>
    <a href="/signup" class="btn btn-default"><span class="fa fa-user"></span> Local Signup</a>
    </div>
    </div>
    </body>
    </html>
    
    This is our /view/index.ejs file.

    Login Form views/login.ejs:

    <!doctype html>
    <html>
    <head>
    <title>Node Authentication</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> <!-- load bootstrap css -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css"> <!-- load fontawesome -->
    <style>
    body        { padding-top:80px; }
    </style>
    </head>
    <body>
    <div class="container">
    
    <div class="col-sm-6 col-sm-offset-3">
    
    <h1><span class="fa fa-sign-in"></span> Login</h1>
    
    <!-- show any messages that come back with authentication -->
    <% if (message.length > 0) { %>
    <div class="alert alert-danger"><%= message %></div>
    <% } %>
    
    <!-- LOGIN FORM -->
    <form action="/login" method="post">
    <div class="form-group">
    <label>Email</label>
    <input required type="text" class="form-control" name="email">
    </div>
    <div class="form-group">
    <label>Password</label>
    <input required type="password" class="form-control" name="password">
    </div>
    <button type="submit" class="btn btn-warning btn-lg">Login</button>
    </form>
    
    <hr>
    
    <p>Need an account? <a href="/signup">Signup</a></p>
    <p>Or go <a href="/">home</a>.</p>
    </div>
    </div>
    </body>
    </html>
    This is our /view/login.ejs file.

    Signup Form views/signup.ejs:

    <!doctype html>
    <html>
    <head>
    <title>Signup</title>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css"> <!-- load bootstrap css -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css"> <!-- load fontawesome -->
    <style>
    body        { padding-top:80px; }
    </style>
    </head>
    <body>
    <div class="container">
    
    <div class="col-sm-6 col-sm-offset-3">
    
    <h1><span class="fa fa-sign-in"></span> Signup</h1>
    
    <!-- show any messages that come back with authentication -->
    <% if (message.length > 0) { %>
    <div class="alert alert-danger"><%= message %></div>
    <% } %>
    <!-- LOGIN FORM -->
    <form action="/signup" method="post">
    <div class="form-group">
    <label>Email</label>
    <input required type="text" class="form-control" name="email">
    </div>
    <div class="form-group">
    <label>Password</label>
    <input required type="password" class="form-control" name="password">
    </div>
    <button type="submit" class="btn btn-warning btn-lg">Signup</button>
    </form>
    <hr>
    <p>Already have an account? <a href="/login">Login</a></p>
    <p>Or go <a href="/">home</a>.</p>
    </div>
    </div>
    </body>
    </html>
    This is our /view/signup.ejs file.

Authenticating With Passport Locally:

Finally! We have finally set up our application and have gotten to the authentication part. So far we have installed our packages, set up our application, connected to our database, created our routes, and created our views.
Now we will create our user model, configure passport for local authentication, and use our configured passport to process our login/signup forms.

User Model:

For local accounts, we will be keeping email and password. You can change these fields out to be whatever you want. You can authenticate locally using username and password (passport-local actually uses username by default but we'll change that to email).
var mongoose = require('mongoose');
var bcrypt   = require('bcrypt-nodejs');

// define the schema for our user model
var userSchema = mongoose.Schema({

    local            : {
        email        : String,
        password     : String,
    },
    facebook         : {
        id           : String,
        token        : String,
        name         : String,
        email        : String
    },
    twitter          : {
        id           : String,
        token        : String,
        displayName  : String,
        username     : String
    },
    google           : {
        id           : String,
        token        : String,
        email        : String,
        name         : String
    }

});

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

// checks if password is valid
userSchema.methods.validPassword = function(password) {
    return bcrypt.compareSync(password, this.local.password);
};

// create the model for users and expose it to our app
module.exports = mongoose.model('User', userSchema);
This is our app/models/user.js file.
Our model is done. We will be hashing our password within our user model before it saves to the database. This means we don't have to deal with generating the hash ourselves. It is all handled nicely and neatly inside our user model.

Configuring Passport for Local Accounts:

All the configuration for passport will be handled in config/passport.js. We want to keep this code in its own file away from our other main files like routes or the server file. I have seen some implementations where passport will be configured in random places. I believe having it in this config file will keep your overall application clean and concise.
var LocalStrategy = require('passport-local').Strategy;
var User= require('../app/models/user');

module.exports = function(passport) {
//passport  serialize and unserialize users out of session
passport.serializeUser(function(user, done) {
done(null, user.id);
});

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

// ======================SIGNUP ===========================
passport.use('local-signup', new LocalStrategy({
// by default, local strategy uses username and password, we will override with email
usernameField : 'email',
passwordField : 'password',
passReqToCallback : true 
// allows us to pass back the entire request to the callback
},
function(req, email, password, done) {
 // User.findOne won't fire unless data is sent back
process.nextTick(function() {

// find a user whose email is the same as the forms email
User.findOne({ 'local.email' :  email }, function(err, user) {
if (err)
return done(err);

if (user) {
return done(null, false, req.flash('signupMessage', 'That email is already taken.'));
} else {

// if there is no user with that email -create the user
var newUser = new User();

// set the user's local credentials
newUser.local.email    = email;
newUser.local.password = newUser.generateHash(password);

// save the user
newUser.save(function(err) {
if (err)
throw err;
return done(null, newUser);
});
}
});    

});

}));

// =================LOCAL LOGIN ======================================

passport.use('local-login', new LocalStrategy({
usernameField : 'email',
passwordField : 'password',
passReqToCallback : true 
},
function(req, email, password, done) { 
// find a user whose email is the same as the forms email
User.findOne({ 'local.email' :  email }, function(err, user) {
if (err)
return done(err);
if (!user)
return done(null, false, req.flash('loginMessage', 'No user found.')); // req.flash is the way to set flashdata using connect-flash
if (!user.validPassword(password))
return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); // create the loginMessage and save it to session as flashdata
// all is well, return successful user
return done(null, user);
});
}));

};

So far, we created our passport object in server.js, and then we pass it to our config/passport.js file. This is where we configure our Strategy for local.
This is also the file where we create the serializeUser and deserializeUser functions to store our user in session.
We have now provided a strategy to passport called local-signup. We will use this strategy to process our signup form. Let's open up our app/routes.js and handle the POST for our signup form.
// app/routes.js
...

    // process the signup form
    app.post('/signup', passport.authenticate('local-signup', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/signup', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }));

...
With users able to sign up, let's give them a way to login.We have provided a strategy to passport called local-login. We will use this strategy to process our login form. We can check if a user exists, if the password is wrong, and set flash data to show error messages. Let's open up our app/routes.js and handle the POST for our login form.
// app/routes.js
...

    // process the login form
    app.post('/login', passport.authenticate('local-login', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/login', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }));

...
If you try to login with a user email that doesn't exist in our database, you will see the error. Same goes for if your password is wrong.

Displaying User and Secure Profile Page views/profile:

Now we have functional signup and login forms. If a user is successful in authenticating they will be redirected to the profile page. If they are not successful, they will go home. The last thing we need to do is make our profile page.
<!doctype html>
<html>
<head>
<title>Profile</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.min.css">
<style>
body{ padding-top:80px; word-wrap:break-word; }
</style>
</head>
<body>
<div class="container">

<div class="page-header text-center">
<h1><span class="fa fa-anchor"></span> Profile Page</h1>
<a href="/logout" class="btn btn-default btn-sm">Logout</a>
</div>

<div class="row">

<!-- INFORMATION -->
<div class="col-sm-6">
<div class="well">
<h3><span class="fa fa-user"></span> Local</h3>

<p>
<strong>id</strong>: <%= user._id %><br>
<strong>email</strong>: <%= user.local.email %><br>
<strong>password</strong>: <%= user.local.password %>
                    </p>

</div>
</div>

</div>

</div>
</body>
</html>

Conclusion:

We've built a brand new application from scratch and have the ability to let users signup/register and login. We even have support for flash messages, hashing passwords, and requiring login for some sections of our site using route middleware.
You can refer to my github repo for the complete setup.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!