How To Use Passport.js: Node.js Authentication Feature 🔒

Shivank MitraShivank Mitra
7 min read

Introduction

Let me introduce you to Passport.js. Passport.js is an npm package that helps you implement user authentication in your Node.js applications. It simplifies the process of persisting user sessions as well. It contains hundreds of authentication strategies too, i.e., options to authenticate using Google, Facebook, Twitter and more. In this article, I'll be exploring two options: regular email login. Understanding basic email login before delving into third-party Authentication is crucial. I won't be building out a full-fledged app by connecting it to a database like MongoDB because my main aim is to explain the functionality of this lightweight library.

I struggled with understanding the concepts of the Passport.js library and implementing it for 1.5 years! Honestly, most of the YouTube videos and blog posts out there use outdated versions of Node or Passport which does not work on current systems. Also, they don't explain the semi-cryptic code of Passport.js. That's what I want to explore today.

Email Authentication

Make a new folder for this project called whatever you want. Inside the terminal, run the following command:

npm install express express-session express-flash passport passport-local ejs

Once that's done, open up the folder in your favourite code editor (mine is VSCode 😛) and create a file called index.js.

Add the following set-up code:

const express = require('express')
const app = express()
const passport = require('passport')
const flash = require('express-flash')
const session = require('express-session')

const users = []

app.listen(3000, () => {
  console.log('Running on http://localhost:3000/')
})

We're just importing the modules that we installed and running the code on port 3000 here. I expect you to know this basic NodeJS setup. Also, we're creating an array called users which holds the users in our application, temporarily. Of course, use a database if you're making a real project, but this is just a tutorial.

To set up our Passport.js strategy for this application, add the following code in right after the users array:

const LocalStrategy = require('passport-local').Strategy

function initialize(getUserByEmail, getUserById) {
  const authenticateUser = async (email, password, done) => {
    const user = getUserByEmail(email)
    if (user == null) {
      return done(null, false, { message: 'No user with that email' })
    }

    if (password === user.password) {
      return done(null, user)
    } else {
      return done(null, false, { message: 'Password incorrect' })
    }
  }

  passport.use(new LocalStrategy({ usernameField: 'email' }, authenticateUser))
  passport.serializeUser((user, done) => done(null, user.id))
  passport.deserializeUser((id, done) => {
    return done(null, getUserById(id))
  })
}

Let's see what we are doing here:

  1. We're creating a function initialize() which takes in two functions as parameters, getUserByEmail and getUserById. I'll discuss these functions later.

  2. We're creating an asynchronous function called authenticateUser which takes in an email, password and a callback called done, which is built into the Passport.js library itself.

  3. To check whether a user exists or not, we're using the getUserByEmail function passed as a parameter. If the user doesn't exist, we terminate the function with return done() which takes three parameters. This part of the code is often not explained in tutorials. The first part takes in an error, which is null in our case. The second parameter is the result, which could be either false or the user object that we find. We don't find any user object, so it's false. The third parameter is often not used, but because we've installed express-flash, the third parameter displays a message using the aforementioned library to notify the user on the frontend that no user was found.

  4. If the user object wasn't null, which means a user must have been found. Then we compare if the password entered in the form matches the user password we have stored. Note that this method of storing passwords is pretty unsafe, and you should probably use a library like bcrypt and its bcrypt.compare() function to hash and compare passwords.

  5. If the password matches, we return the done() function with the user object. When we pass in the user object like this, Passport knows that it should save the user in the local session.

  6. If it doesn't match, we return done() with the message 'Password incorrect'.

  7. After the authenticateUser function, we're setting up Passport to use the LocalStrategy that we get from passport-local. For the first parameter, we're saying that the field for the username on our HTML form is not the typical username, but instead email. We're also passing in our authenticateUser function.

  8. We're using passport.serializeUser() to define a way to serialize, i.e., store our user object in the session. We're serializing the user using the user id.

  9. For deserializing, i.e., obtaining a user object from the session data, we're using the getUserById function to search through our users for a matching id and we return the user found.

Sheesh! That's a lot of code to go through and understand. All that is left is to use the initialize function we created. To do that, add in the following code after the function declaration:

initialize(
  email => users.find(user => user.email === email),
  id => users.find(user => user.id === id)
)

app.set('view-engine', 'ejs')
app.use(express.urlencoded({ extended: false }))
app.use(flash())
app.use(session({
  secret: 'secret',
  resave: false,
  saveUninitialized: false
}))
app.use(passport.initialize())
app.use(passport.session())

We're creating and passing two functions for getUserByEmail and getUserById using the Array.find() method. Then we're configuring our app and integrating express-flash, express-session and passport too.

Now that we've configured our app and Passport.js accordingly, let's add in our routes and ejs files.

Routes

We know that we need three pages, the login page, the register/sign-up page and a homepage that will welcome the user. The data that we get from the login page needs to be used for the homepage. We also need a way to be able to check whether or not the user is currently logged in. Let's create two functions to accomplish that:

function checkAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next()
  }

  res.redirect('/login')
}

function checkNotAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return res.redirect('/')
  }
  next()
}

In these functions, we're checking if the request is authenticated or not. We're redirecting the request accordingly. Let's use these functions and create some routes now:

app.get('/', checkAuthenticated, (req, res) => {
  res.render('index.ejs', { name: req.user.name })
})

app.get('/login', checkNotAuthenticated, (req, res) => {
  res.render('login.ejs')
})

app.post('/login', checkNotAuthenticated, passport.authenticate('local', {
  successRedirect: '/',
  failureRedirect: '/login',
  failureFlash: true
}))

app.get('/register', checkNotAuthenticated, (req, res) => {
  res.render('register.ejs')
})

app.post('/register', checkNotAuthenticated, async (req, res) => {
    users.push({
      id: Date.now().toString(),
      name: req.body.name,
      email: req.body.email,
      password: req.body.password
    })
    res.redirect('/login')
})

app.get('/logout', (req, res) => {
  req.logOut((err)=>err ? console.log(err) : null)
  res.redirect('/login')
})

Let me explain what's going on here:

  1. At the root route '/', we're checking if the user is authenticated or not. If so, we're rendering index.ejs (which we'll be creating soon) and passing in the req.user.name for the frontend data. If you try to console.log(req.user), you'll notice that it contains the user data that Passport stores in the session. This is how we're sending the data to the frontend.

  2. The app.post('/login') occurs when the user submits the login form. Have a look at the parameters we're passing into it.

  3. For app.post('/register'), we're creating a user object and pushing it into users. For the id, the current date is being used, since it's always unique.

  4. For logging out the user we use req.logOut(). Then we redirect the user to the login page and the session gets wiped out.

Our routes are looking awesome! Add the following ejs files into a folder called views inside the root folder.

index.ejs

<h1>Hi <%= name %></h1>
<form action="/logout" method="GET">
  <button type="submit">Log Out</button>
</form>

login.ejs

<h1>Login</h1>
<% if (messages.error) { %>
  <%= messages.error %>
<% } %>
<form action="/login" method="POST">
  <div>
    <label for="email">Email</label>
    <input type="email" id="email" name="email" required>
  </div>
  <div>
    <label for="password">Password</label>
    <input type="password" id="password" name="password" required>
  </div>
  <button type="submit">Login</button>
</form>
<a href="/register">Register</a>

register.ejs

<h1>Register</h1>
<form action="/register" method="POST">
  <div>
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required>
  </div>
  <div>
    <label for="email">Email</label>
    <input type="email" id="email" name="email" required>
  </div>
  <div>
    <label for="password">Password</label>
    <input type="password" id="password" name="password" required>
  </div>
  <button type="submit">Register</button>
</form>
<a href="/login">Login</a>

These files are super straightforward. Note the error flashing and username displaying functionality in login.ejs and index.ejs respectively.

If you run npm index.js now, it should work elegantly! (At least it is on my computer. If you're facing any issues, comment it)


Damn, Passport.js is tough! Once you get the hang of it though, authentication in NodeJS is light work. I hope I've taught you the concepts of Passport.js authentication well and if I did, leave a like and comment.

Thanks for reading! If you want to read more, check out my blog here.

6
Subscribe to my newsletter

Read articles from Shivank Mitra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shivank Mitra
Shivank Mitra

Full Stack JavaScript Dev 👨‍💻 Basketball Player 🏀 Always improving 💯