Complete Tutorial on Node.js Passport [Practical Examples]


NodeJS

Author: Steve Alila
Reviewer: Deepak Prasad

Node.js passport simplifies user authentication and authorization when building web applications. Despite the usefulness of passport.js, you may fail to maximize its potential without understanding HTTP headers, middleware, sessions, the concept of strategies, and user authentication.

This tutorial explains the passport.js must-knows in plain English. After understanding the key concepts, you will practice them by building a simple login application. What is more? Find out below.

 

Understanding Node.js Passport module in layman's terms

Technically, Nodejs passport is a module for user authentication.

A module is code residing in a file away from the one you are currently modifying. There are two main types of modules: global and custom modules.

A global module comes with Node.js, and you don't install it with the node package manager (npm). A custom module is user-created and lives in the npm. Passport.js is an example of a custom module.

npm install passport passport-local

It is worth noting the difference between authentication and authorization. Authentication is knowing the users of your application. On the other hand, authorization entails controlling the information accessible to each user.

Nodejs passport stands between a browser (also known as the client or user-agent) and the server, identifying logged-in users.

It monitors requests to authenticate a user, appending different properties to them. Nodejs passport is thus an example of middleware. A middleware is a block of code that controls how a part of code interacts with another.

Nodejs passport exists in strategies. A passport strategy is a middleware designed to interact differently depending on the platform and its access mode.

Different developers build and post the Nodejs passport strategies, including their documentation. As a result, some strategies could have better documentation than others.

However, the concept of user-authentication with Node.js passport is the same. This tutorial points out what the docs may not tell you.

Now that you know the significance of Nodejs passport in your application and the challenges incurred while adopting it, you should understand client-server interaction before applying the module.

 

How Express.js powers Node.js passport

Before learning the role of express sessions in user authentication with Nodejs passport, it would be best to find out how Express.js powers the authentication technology in the background.

Think of express as a pool of middleware. Unlike typical JavaScript functions, the middleware accepts arguments whose representations are predetermined by their parsing order: error handler, request object, response object, and the next middleware handler.

The type of middleware determines the number of arguments to parse. A route middleware is complete with request and response objects. Think of routes as middleware incorporating HTTP headers: GET, POST, PUT and DELETE.

Other middleware apart from error-handlers accepts the request, response, and next middleware handlers. Lastly, an error-handling middleware accepts the four parameters in the above order.

The order of the middleware calling determines their effectiveness. Express.js wants you to put a middleware in the global use() middleware if you wish the middleware to be effective throughout the application. Otherwise, run or reference the middleware within another sub-middleware.

You can also attach custom properties to a middleware, visible in subsequent middleware. That is how Nodejs passport links particular data to request objects, the state being visible across your application.

 

The role of sessions in Node.js passport user authentication

Nodejs passport authenticates a user through express sessions.

The client sends HTTP requests to one of the routes. The express session middleware initializes a session on the server.

Unlike a (client-side) cookie that identifies the client by attaching itself to HTTP requests, a (server-side) session authenticates a user using a secret key.

Next, the middleware grabs the session id and sets it to the request cookie. The middleware then stores the cookie in the set-cookie HTTP response header returning to the client. Once the cookie reaches the browser, it holds it and resends the cached data with subsequent requests.

During the subsequent request, express session middleware grabs the cookie value and matches it to that stored in the database using the store attribute.

Let's see the authentication practically using Nodejs passport with the local strategy.

 

Lab setup to practice Nodejs passport local authentication

We will build a simple login using Express.js, authenticating the user with Nodejs passport's local strategy. It would be best if you understood how Nodejs modules and filesystem work, using Express.js ejs and routing to follow the following sections of this tutorial.

I will explain the specific project files that directly interact with Nodejs passport. You can get the code on GitHub before proceeding. Clone the repository and open the files using your preferred code editor.

Complete Tutorial on Node.js Passport [Practical Examples]

Install the packages

npm i

and nodemon globally or as a devDependency

npm i -g nodemon

Complete Tutorial on Node.js Passport [Practical Examples]

Here is what each installed module does.

bcryptjs hashes and verifies user passwords. connect-mongo stores session data in MongoDB. ejs is the templating engine, and we are using its express layouts. express-session caches client and logged-in user data. method-override handles DELETE requests when logging out the user from the session.

mongoose connects us to MongoDB. passport is the authentication middleware, and we are using its local strategy in this application. dotenv stores sensitive files, preventing us from pushing them to a remote repository.

nodemon refreshes our server, relieving us of the burden of restarting the server whenever we make server-side changes to the code.

 

Configure environment variables

Create .env file and configure PORT, SECRET and MONGODBURI.

PORT = 3000
SECRET = "secret"
MONGODBURI = "mongodb://127.0.0.1:27017/nodejs_passport"

I am using a local MongoDB database instance called nodejs_passport. I do the connection it in the db.js file and log the result if the connection succeeds.

import mongoose from 'mongoose'

const dbConnection = async () => {
    try {
        const conn = await mongoose.connect(process.env.MONGODBURI)
        console.log(`Db runs in ${conn.connection.host}`)
    } catch {
        process.exit(1)
    }
}

export default dbConnection

Let's start the server to see if everything works as we expect.

// start the server
npm start

// output
Listening on port 3000
Db runs in 127.0.0.1

Complete Tutorial on Node.js Passport [Practical Examples]

 

Model

I tell MongoDB the blueprint of data to store: username, email, password, and registrationDate in the models/User.js

import mongoose from 'mongoose'

const userSchema = mongoose.Schema({
    username: { type: 'string', required: true },
    email: { type: 'string', required: true, unique: true },
    password: { type: 'string', required: true },
    registrationDate: { type: Date, default: Date.now }
})

const User = mongoose.model('User', userSchema)
export default User

I receive the data using the form in views/register.ejs.

<div class="register">
    <form autocomplete="off" action="/users/register" method="post">
        
        <label for="username">Username: </label><br>
        <input type="text" name="username" id="username" required ><br><br>

        <label for="email">Email: </label><br>
        <input type="email" name="email" id="email" required ><br><br>

        <label for="pwd">Password: </label><br>
        <input type="password" name="pwd" id="pwd" required ><br><br>

        <label for="pwdConf">Confirm Password: </label><br>
        <input type="password" name="pwdConf" id="pwdConf" required ><br><br>

        <button>Register</button>
    </form>
</div>

In the form, I tell the client to make a POST request to /users/register, that is found in routes/users.js.

router.post('/register', async (req, res) => {
    const { username, email, pwd, pwdConf } = req.body
    
    // VALIDATION
    const errors = []

    if(pwd !== pwdConf) errors.push(`Passwords don't match`)
    
    const emailTaken = await User.findOne({ email })
    if(emailTaken) errors.push(`Email taken!`)
    
    if(errors.length > 0) res.redirect('/register', { errorMessage: errors})

    // REGISTRATION
    const hashpwd = await bcrypt.hash(pwd, 12)
    let user = new User({ username, email, password: hashpwd })

    try {
        await user.save()
        res.redirect('/users/login')
    } catch {
        res.redirect('/', { message: "There was a problem register the user" })
    }
})

I register the user and redirect them to the login page.

<div class="register">
    <form autocomplete="off" action="/users/login" method="post">

        <label for="email">Email: </label><br>
        <input type="email" name="email" id="email" required><br><br>

        <label for="password">Password: </label><br>
        <input type="password" name="password" id="password" required><br><br>

        <button>Login</button>
    </form>
</div>

The form, views/login.ejs, collects the user details named email and password and sends them to the /users/login route, accessible through routes => users.js.

router.post('/login', passport.authenticate('local', {
    successRedirect: '/',
    failureRedirect: '/users/register'
}))

And that is where Nodejs passport starts user authentication. I am telling passport middleware, "Hey, passport. Authenticate this user whose details are coming through the form. Use your local strategy. If you identify the user, let her see the landing page. Otherwise, redirect her to the register page."

Nodejs passport does all the magic in the passport.js file, as explained below.

 

Authentication with Nodejs passport module (step-by-step)

Import modules

I import the modules.

import local_strategy from "passport-local"
import bcrypt from "bcryptjs"
import User from './models/User.js'

I store the Strategy instance in LocaStrategy.

const LocalStrategy = local_strategy.Strategy

 

Authenticate the user

I create the main function, authenticateUser()for user authentication, serialization, and deserialization. I will feed the function with the passport in the entry file: app.js. Before that let's focus on how the function authenticates the user.

const authenticateUser = (passport) => {}

I tell the function to globally use the passport middleware, which goes ahead to instantiate a LocalStrategy. The object, in turn, takes any custom fields and a verification function.

passport.use(new LocalStrategy(customFields, verifyCallback))

Let's donate the attention to the customFields.

Passport intercepts the login form data and stores it in Fields, for example, username + Field = usernameField. In this case, passport.js attaches the incoming form names as username, email, or password.

So, if your form sends the password as any other name other than its default password, for instance, pwd, passport.js won't authenticate the user.

I use the options

const customFields = {
    usernameField: 'email',
    passwordField: 'password',
}

to inform passport to accept a customized representation other than what its defaults. For instance, I am logging in the users with emails and passwords, NOT the default username and password.

The callback function accepts three parameters: email and password and another callback function, conventionally named done.

const verifyCallback = (email, password, done) => {

    User.findOne({ email })
    .then (user => {
        if(!user){ return done(null, false) } 
        else {
            const isValid = bcrypt.compareSync(password, user.password)
            if(!isValid){
                return done(null, false)
            }else{
                return done(null, user)
            }
        }
    })
}

The done function tracks authentication errors and the user. I check the user in MongoDB using their email.

If the verifyCallback function fails to find a user, it returns false with no errors. If a user with the email from the form exists in the database, they get logged in. Before that, Nodejs passport checks if the user has typed the correct password using bcryptjs module that we imported earlier. If the password is valid, the user gets logged in to the session.

Note: Maintain the order when verifying the password: (password, user.password) NOT (user.password, password).

 

Cache the user details in sessions

For the client to cache the information, passport.js serializes the user. The most straightforward implication of user serialization is, "Hey passport, grab the authenticated user's id and store in the session in the database."

When the session expires, passport.js deserializes the user. It implies, "Run a check in the database. If you locate a user with the given id, remove their details from the session. If the process doesn't proceed as expected, catch the errors."

Nodejs passport local strategy

Now that Nodejs passport has got all it needs to authenticate the user, let's run the authenticateUser() function in the app.js and make Nodejs passport effective in the entire application.

 

Globalize the transactions

Finally, let us connect everything we have configured to app.js.

We are importing the installed modules.

import dotenv from 'dotenv'
import passport  from 'passport'
import express from 'express'
import ejsLayout from  'express-ejs-layouts'
import methodOverride  from 'method-override'
import session  from 'express-session'
import MongoStore from 'connect-mongo'

and local ones

import authenticateUser from './passport.js'
import userRoutes  from './routes/users.js'
import indexRoute  from './routes/index.js'
import db from './db.js'

Instantiate an express server, start the database connection, and the authenticateUser(passport)function with passport as an argument.

Scrolling down the page, you get to configure the sessions.

app.use(session({
    secret: process.env.SECRET,
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({ mongoUrl: process.env.MONGODBURI }),
    cookie: {
        maxAge: 1000* 60 * 60 * 24
    }
}))

// passport session
app.use(passport.initialize())
app.use(passport.session())

The secret validates a session. resave and saveUninitialized ask if we want to save the session if nothing changes in the session. The store property saves the session as a collection in MongoDB. We have set up a cookie that expires after a day.

passport.initialize() activates the passport middleware so it doesn't go stale while switching between various routes. passport.session() implements the express session middleware in Nodejs passport.

 

Conclusion

Despite the usefulness of the Nodejs passport, you may never maximize its potential if you use it without understanding what each line does. Now that you have a deep knowledge of the authentication module, go ahead and implement the strategy of your choice.

 

Steve Alila

Steve Alila

He specializes in web design, WordPress development, and data analysis, with proficiency in Python, JavaScript, and data extraction tools. Additionally, he excels in web API development, AI integration, and data presentation using Matplotlib and Plotly. You can connect with him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

2 thoughts on “Complete Tutorial on Node.js Passport [Practical Examples]”

  1. Very helpful. I was able to modify your code examples to use azure data tables in lieu of mongoose (they are very similar)

    Reply

Leave a Comment