DEV Community

Cover image for How to use PassportJS for authentication in NodeJS
Arinze Obi
Arinze Obi

Posted on • Edited on

How to use PassportJS for authentication in NodeJS

NB:

  1. Basic knowledge of node.js is needed to follow along with this article
  2. You should be on the root path of the project when typing in the commands from the command line snippet

The majority of the codebases I've worked on over the years have always favoured using JSON web-tokens (JWT) or Authentication-as-a-Service platforms (Auth0, Okta etc) for authentication logic.
These are indeed excellent choices! however, on smaller projects I find these to always seem to be overkill. Recently I started working on a chrome extension that performs social sign-in using twitter OAuth API and decided to use passport.js to outsource some of the heavy lifting involved in setting up authentication. Below I walk through the benefits and step-by-step guide in getting started with passport.js for authentication

The Why

Authentication is a very delicate process in the creation of software and having the right strategy to use for your application can be a chore. In order not to get caught up in a dilemma of the perfect authentication flow to use that would handle millions of users, I like to stick to simpler tools that would serve my end users effectively and only scale when the need arises (and not a moment before). For a frontend heavy application like the chrome extension I'm working on, I found setting up authentication with passport.js to be the more effective option for my use case as I will require very little usage of the backend.

What is PassportJS

According to the official docs

Passport is authentication middleware for node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.

Let's break this down so we understand it at a more fundamental level.

passport is authentication middleware for node.js: A middleware is just a function that runs in between a request/response cycle and node.js is a runtime environment that enables us run javascript outside the browser.

can be unobtrusively dropped in to any express-based application: This means we can use passport in any node.js application in a way that keeps our code lean, clean and easy to maintain.

support authentication using a username and password, Facebook, Twitter: passport.js gives you the flexibility to authenticate users with username/password, google, twitter and more. It does this by using strategies which are installed as separate packages from the npm directory and there are more than 500 strategies that can work with passport!

To top it all off this package is 81kB! For context the google-auth-library which is the official library for implementing google OAuth has a size of 496kB. Pretty easy to see why I am using passport.js on this project

Project Scope and Setup

Without wasting too much time let's jump into the scope of the project.
Today we will be making a simple app that
allows users to login, view the current time of day and logout

First thing's first, let's setup our server.
(PS: You might want to make sure you have nodejs installed on your machine first)

Create folder

mkdir timely # create folder for project
cd timely # navigate into folder
touch index.js # create main entry point into your server
npm init -y # initialize npm
Enter fullscreen mode Exit fullscreen mode

Install dependencies

npm install passport express express-session passport-local ejs # install dependencies
npm install nodemon --save-dev #install peer dependencies
Enter fullscreen mode Exit fullscreen mode

Setup User Interface

We're going to be using EJS as our templating engine so we can dynamically display user content on our website. If you're not quite sure what templating engines are, you can get an idea through on this amazing article.

mkdir views # create views folder
cd views # navigate into views folder
touch authenticate.ejs && touch index.ejs # create out home and authentication page
Enter fullscreen mode Exit fullscreen mode

Now we will be adding the public folder which houses our static assets for the project

mkdir public # create public folder 
cd public # navigate into public folder 
touch main.js && touch styles.css
Enter fullscreen mode Exit fullscreen mode

I've included the content for main.js and styles.css which you can copy over here

Locate the index.ejs file and replace it's contents with the following as well

<!-- navigate into timely/views/index.ejs and insert the following -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/styles.css" />
    <script defer src="/main.js"></script>
    <title>Timely | Who needs a rolex anyway 🤷‍♂️</title>
  </head>
  <body>
    <div class="header">
      <h1>Timely 🕰</h1>
    </div>
      <div class="container">
        <h1>Welcome User</h1>
        <div class="content"></div>
      </div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Create a Server

// Navigate into timely/index.js
const express = require('express');
const path = require('node:path');
const app = express();


app.use(express.static(path.join(__dirname, 'public'))); // Require static assets from public folder
app.set('views', path.join(__dirname, 'views')); // Set 'views' directory for any views being rendered res.render()
app.engine('html', require('ejs').renderFile); // Set view engine as EJS
app.set('view engine', 'ejs');

app.get('/', (req, res) => res.render('index'));

app.listen(3000, () => console.log('server is running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

Setup scripts in package.json

{
  "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1",
   "start": "node index.js",
   "start:dev": "nodemon index.js"
 }
}
Enter fullscreen mode Exit fullscreen mode

Run script

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

At this point we should get back a large text that reads "Welcome User" along side the current date and time when we visit http://localhost:3000/

Home page of timely passportjs app

Looks great! but we want only logged in users to have access to the time and date. so lets create an authentication page for users that aren't logged in.
Locate the authentication.ejs file inside your views folder and replace it's content with the following

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/styles.css" />
    <title>Timely | Who needs a rolex anyway 🤷‍♂️</title>
  </head>
  <body>
    <div class="header">
      <h1>Timely 🕰</h1>
    </div>
    <form action="/log-in" method="POST" class="container">
      <h1>please log in</h1>
      <div>
        <label for="username">Username</label>
        <input name="username" placeholder="username" type="text" />
      </div>
      <div>
        <label for="password">Password</label>
        <input name="password" type="password" />
      </div>
      <button>Log In</button>
    </form>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Before we write out any more code, lets understand the passport packages we installed and how they fit together

passport: This is the main passport module that helps us attach the information of a current logged in user to the req.user object

passport-local: Passport.js works in collaboration with strategy packages to perform the authentication flows you need for your application. We will be using passport-local for this as we will be authenticating our users with username and password only

express-session: This will enable us store and manage sessions of currently logged in users on the server

Now that we have a little idea of how passport works, let's create our passport middleware strategy. This will be the logic behind adding the user information into our req.user object. In your root folder create the following directories

mkdir strategy && cd strategy # create and move into strategy folder
touch local.js # create strategy file 
Enter fullscreen mode Exit fullscreen mode

NB: On real world projects it's best to hash user passwords before storing them, for improved security and also use an actual database for storing user information. Here I will be storing user information in a json file on the system to avoid network calls and keep the sole focus of the article on passport.js.

Inside local.js replace it's content with the following

const path = require('node:path'); // path module to find absolute paths on the system 
const fs = require('node:fs'); // file system module to manipulate files
const passport = require('passport'); // the main star of the show
const LocalStrategy = require('passport-local'); // the co-protagonist in this sequel 

const dbpath = path.join(__dirname, 'db.json'); // path to our custom in-house json database

passport.serializeUser((user, done) => { 
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  if (!fs.existsSync(dbpath)) { // if db does not exist, create db
    fs.writeFileSync(dbpath, JSON.stringify({ users: [] }));
  }

  const db = JSON.parse(fs.readFileSync(dbpath, { encoding: 'utf-8' }));

  let user = db.users.find((item) => item.id === id);

  if (!user) {
    done(new Error('Failed to deserialize'));
  }

  done(null, user);
});

passport.use(
  new LocalStrategy(async (username, password, done) => {
    if (!fs.existsSync(dbpath)) { // if db.json does not exist yet, we create it
      fs.writeFileSync(dbpath, JSON.stringify({ users: [] }));
    }

    const db = JSON.parse(fs.readFileSync(dbpath, { encoding: 'utf-8' }));

    let user = db.users.find((item) => {
      return item.username === username && item.password === password;
    });

    if (!user) {
      user = {
        id: Math.floor(Math.random() * 1000), // generate random id between numbers 1 - 999
        username,
        password,
      };

      db.users.push(user);
      fs.writeFileSync(dbpath, JSON.stringify(db));
    }

    done(null, user);
  })
);
Enter fullscreen mode Exit fullscreen mode

this file will be run each time a request is made to our server. Let's breakdown what each method does exactly

passport.use: This is the first point of contact and is responsible for creating our users. It is run once and after successfully authenticating our user, it passes this information unto the next middleware which is the passport.serializeUser

passport.serializeUser: This creates a session in passport with our key being user.id. It's quite important for our sessionIDs to be unique as well to avoid conflict

passport.deserializeUser: On each request, This method receives our sessionID and scans our database to get the user information of the currently logged in user then attaches that information to the req.user object

Now That we know how passport works lets finally include it into the project which will enable us identify logged in users and re-route their requests accordingly

const express = require('express');
const passport = require('passport');
const session = require('express-session');
const path = require('node:path');
const app = express();
require('./strategy/local'); // passport strategy will listen to every request

app.use(
  session({
    secret: 'SOME SECRET', // secret key to sign our session
    resave: false,
    saveUninitialized: false,
    cookie: {
      maxAge: 1000 * 60 * 60 * 24, // TTL for the session
    },
  })
);
// initialize passport package
app.use(passport.initialize());
// initialize a session with passport that authenticates the sessions from express-session
app.use(passport.session());

app.use(express.urlencoded({ extended: false }));

app.use(express.static(path.join(__dirname, 'public'))); // Require static assets from public folder
app.set('views', path.join(__dirname, 'views')); // Set 'views' directory for any views being rendered res.render()
app.engine('html', require('ejs').renderFile); // Set view engine as EJS
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
  if (!req.user) {
    // if passport does not have record of this user, we redirect them to the authentication page
    res.render('authenticate');
  } else {
    res.render('index', { user: req.user });
  }
});

app.post(
  '/log-in',
  passport.authenticate('local', {
    // on Initial login, passport will redirect to either of these routes
    successRedirect: '/',
    failureRedirect: '/',
  })
);

app.listen(3000, () => console.log('server is running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

If we visit http://localhost:3000/ on our browser again, we are immediately routed to the authentication page.

Authentication page using passportjs

Upon sign-up we are assigned a session cookie which you can view in our browser devtools console (right click on the page -> inspect -> Application -> cookies -> http://localhost:3000).
Each time a request is sent to the server, this cookie is sent alongside it and from this cookie, passport can correctly identify if we are logged in or not.
To further certify this point, you can go ahead and delete the cookie from the devtools console then refresh the page - In an instant you are immediately re-routed to the authentication page.

Our homepage looks good but let's add a bit of personality to it by displaying the users username with ejs and also the ability for users to logout.

In index.ejs make the following changes

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/styles.css" />
    <script defer src="/main.js"></script>
    <title>Timely | Who needs a rolex anyway 🤷‍♂️</title>
  </head>
  <body>
    <div class="header">
      <h1>Timely 🕰</h1>
    </div>
    <form class="container" action="/log-out" method="post">
      <h1>Welcome back, <%= user.username %></h1>
      <div class="content"></div>
      <button>Log out</button>
    </form>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now to handle the logout route on the server we can add the following code just above app.listen() method

app.post('/log-out', (req, res) => {
  req.logOut((err) => { // clear out information in passport-session and redirects user
    if (err) {
      res.send('something went wrong');
    }

    res.redirect('/');
  });
});

app.use('*', (req, res) => res.redirect('/')); // to catch and redirect all other routes 
Enter fullscreen mode Exit fullscreen mode

Completed home page of timely passportjs app

NB: You might have noticed that each time you make changes to server files, you are re-routed to the authentication page. This is because sessionIDs are stored in a cache created by express-session and once nodemon restarts the server, the express-session cache is wiped clean hence passport can not recognise us with the previous sessionID.

And that's it! Now we can fully log in and out of our application using passport.js.
I do recommend reading more into their documentations to discover new strategies you can use in your future projects. Till next time, Ciao!

Top comments (0)