How To Use Passport.js: Node.js Authentication Feature 🔒
Easy user authentication for Node.js devs
Table of contents
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:
We're creating a function
initialize()
which takes in two functions as parameters,getUserByEmail
andgetUserById
. I'll discuss these functions later.We're creating an asynchronous function called
authenticateUser
which takes in anemail
,password
and a callback calleddone
, which is built into the Passport.js library itself.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 withreturn done()
which takes three parameters. This part of the code is often not explained in tutorials. The first part takes in an error, which isnull
in our case. The second parameter is the result, which could be eitherfalse
or the user object that we find. We don't find any user object, so it'sfalse
. The third parameter is often not used, but because we've installedexpress-flash
, the third parameter displays a message using the aforementioned library to notify the user on the frontend that no user was found.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 likebcrypt
and itsbcrypt.compare()
function to hash and compare passwords.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.If it doesn't match, we return
done()
with the message 'Password incorrect'.After the
authenticateUser
function, we're setting up Passport to use theLocalStrategy
that we get frompassport-local
. For the first parameter, we're saying that the field for the username on our HTML form is not the typicalusername
, but insteademail
. We're also passing in ourauthenticateUser
function.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.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:
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 thereq.user.name
for the frontend data. If you try toconsole.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.The
app.post('/login')
occurs when the user submits the login form. Have a look at the parameters we're passing into it.For
app.post('/register')
, we're creating a user object and pushing it intousers
. For the id, the current date is being used, since it's always unique.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.