NodeJS Express Tutorial

This tutorial demonstrates how to add user login, logout, and profile to a Node.js Express application.

This tutorial shows how to use PlusAuth with ExpressJS. If you do not have a PlusAuth account, register from here.

Create PlusAuth Client

After you sign up or log in to PlusAuth, you need to create a client to get the necessary configuration keys in the dashboard. Go to Clients and create a client with the type of Regular Web Application

Configure Client

Get Client Properties

You will need your Client Id and Client Secret for interacting with PlusAuth. You can retrieve them from the created client's details.

Configure Redirect and Logout URIs

When PlusAuth authenticates a user, it needs a URI to redirect back. That URI must be in your client's Redirect URI list. If your application uses a redirect URI that is not white-listed in your PlusAuth Client, you will receive an error.

The same thing applies to the logout URIs. After the user logs out, you need a URI to be redirected.

Configure Node.js to use PlusAuth

Let's start to create an ExpressJS application.

We are using async functions on initializing the state. Below you will find snippets wrapped in Immediately-invoked Function Expressions (IIFE), but in the final state, there would be only one.

Create the .env file

Create the .env file in the root of your app and add your PlusAuth variables and values to it.

# .env
PLUSAUTH_ISSUER_URL=YOUR_PLUSAUTH_DOMAIN
PLUSAUTH_CLIENT_ID=YOUR_CLIENT_ID
PLUSAUTH_CLIENT_SECRET=YOUR_CLIENT_SECRET
Do not put the .env file into source control. Otherwise, your history will contain references to your client's secret. If you are using git, create a .gitignore file (or edit your existing one, if you have one already) and add .env to it. The .gitignore file tells source control to ignore the files (or file patterns) you list. Be careful to add .env to your .gitignore file and commit that change before you add your .env
# .gitignore
.env

Install the dependencies

To get started, install the following dependencies.

  • passport - an authentication middleware for Node.js
  • openid-client - an PlusAuth authentication strategy for Passport
  • express-session - a middleware to manage sessions
  • dotenv - a module to load environment variables from a .env file
  • ejs - a simple yet powerful template engine for creating views
# installation with npm
npm install passport openid-client express-session dotenv ejs --save

Configure Express Application

We will configure our Express application in a simple way. We will be using EJS as template engine.

// app.js
const express = require("express");

(async () => {
  const app = express();

  // view engine setup
  app.set("views", path.join(__dirname, "views"));
  app.set("view engine", "ejs");
})();

Configure express-session

Include the express-session module and configure it in app.js. The secret parameter is a secret string that is used to sign the session ID cookie. Please use a custom value.

// app.js

const session = require('express-session');

(async ()=>{
  // express-session config
  const sessionOptions = {
    secret: 'SomeRandomValue', // Change this to a random value
    resave: false,
    saveUninitialized: true
  };

  if (process.env.NODE_ENV === 'production') {
    // Use secure cookies in production. More info at https://www.npmjs.com/package/express-session#cookiesecure
    sess.cookie.secure = true;

    // Uncomment the line below if your application is behind a proxy (like on Nginx, Envoy, Heroku, etc.)
    // app.set('trust proxy', 1);
  }

  app.use(session(sessionOptions));
})();

Configure Passport with the application settings

Include the passport and openid-client modules in app.js. Configure Passport to use a PlusAuth Client with your settings. Use passport.initialize() and passport.session() to initialize Passport with persistent login sessions.

Passing the scope parameter to openid-client strategy with values openid email profile is necessary to access email and the other attributes stored in the user profile.

// app.js

// Load environment variables from .env
require('dotenv').config();

const passport = require('passport');
const { Issuer, Strategy } = require("openid-client");

(async () => {
  const PlusAuthIssuer = await Issuer.discover(process.env.PLUSAUTH_ISSUER_URL);

  const PlusAuthClient = new PlusAuthIssuer.Client({
    PLUSAUTH_CLIENT_ID: process.env.PLUSAUTH_CLIENT_ID,
    PLUSAUTH_CLIENT_SECRET: process.env.PLUSAUTH_CLIENT_SECRET,
    redirect_uris: ["http://localhost:3000/auth/callback"],

    post_logout_redirect_uris: ["http://localhost:3000/auth/logout/callback"],
    response_types: ["code"],
  });

  const PlusAuthStrategy = new Strategy(
    {
      client: PlusAuthClient,
      params: {
        scope: "openid email profile",
      },
      passReqToCallback:  true
    },
    (req, token, user,done) => {
      // Store token in session
      req.session.token = token
      return done(null, user);
    }
  )

  passport.use("PlusAuth", PlusAuthStrategy);

  app.use(passport.initialize());
  app.use(passport.session());
})();

Please make sure you add passport middlewares in your code after the express middleware (app.use(session(sessionOptions)).

Storing and retrieving user data from the session

In a typical web application, the credentials used to authenticate a user are only transmitted during the login request. If authentication succeeds, a session would be established and maintained via a cookie set in the user's browser. Each subsequent request does not contain credentials but rather the unique cookie that identifies the session.

To support login sessions, Passport serializes and deserializes user instances to and from the session. Optionally, you may want to serialize only a subset to reduce the footprint, i.e., user.id.

// app.js

// You can use this section to keep a smaller payload
passport.serializeUser(function (user, done) {
  done(null, user);
});

passport.deserializeUser(function (user, done) {
  done(null, user);
});

Middleware to protect routes

Create an isLoggedIn middleware to protect routes and ensure they are only accessible if logged in.

// app.js
function isLoggedIn(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }

  res.redirect("/auth/login");
}

Implement login, user profile, and logout

In this example, the following routes are implemented:

  • /auth/login triggers the authentication by calling Passport's authenticate method. The user is then redirected to the tenant login page hosted by PlusAuth.
  • /auth/callback is the route that the user is returned to by PlusAuth after authenticating. It redirects the user to the profile page (/user).
  • /profile displays the user's profile.
  • /auth/logout logs the user out of PlusAuth.
  • /auth/logout/callback is the route that the user is returned to by PlusAuth after logging out.

Adding the authentication routes

Below, you will find routes related to authentication.

// app.js

app.use("/auth/login", passport.authenticate("PlusAuth"));

app.use(
  "/auth/callback",
  passport.authenticate("PlusAuth", {
    failureMessage: true,
    failureRedirect: "/error",
    successRedirect: "/profile",
  })
);

app.get("/auth/logout", (req, res) => {
  res.redirect(
    PlusAuthClient.endSessionUrl({ id_token_hint: req.session.token.id_token })
  );
});

app.get("/auth/logout/callback", (req, res) => {
  req.logout();
  res.redirect("/");
});

Create the user profile route

The /profile route (the user's profile) should only be accessible if the user is logged in. We will be using authentication middleware we created in the step Middleware to protect routes

// app.js
app.use("/profile", isLoggedIn, (req, res) => {
  res.render("profile", { user: req.user });
});

Index route

Let's create an index route to serve our application's homepage.

// app.js
app.get("/", function (req, res) {
  res.render("index", { user: req.user });
});

Making the user available in the views

In the views and layouts, it is often necessary to conditionally render content depending on if a user is logged in or not. Other times, the user object might be required to customize the view.

Create a middleware lib/middleware/userInViews.js for this purpose.

// userInViews.js

module.exports = function () {
  return function (req, res, next) {
    res.locals.user = req.user;
    next();
  };
};

Create Views

Homepage

Create a views/index.ejs template.

<!-- views/index.ejs -->

<%- include('header'); %>
<div class="jumbotron">
  <div class="container">
    <h1 class="display-3">Hello, world!</h1>
    <p>This is a template for a simple login/register system.</p>
    <% if(user) { %>
      <a class="btn btn-success btn-lg" href="/profile" role="button">View Profile &raquo;</a>
    <% }else { %>
      <p>To view Profile page please login.</p>
      <a class="btn btn-primary btn-lg" href="/auth/login" role="button">Login/Register &raquo;</a>
    <% } %>
  </div>
</div>
<%- include('footer'); %>

User Profile

Create a views/profile.ejs template.

<!-- views/profile.ejs -->

<%- include('header'); %>
<div class="container">
  <h3>Welcome <%= user.email %>!</h3>
  <pre>User object: <%= JSON.stringify(user, null, 3) %></pre>
</div>
<%- include('footer'); %>

See it in action

That's it. Start your app and point your browser to http://localhost:3000. Follow the Log In link to log in or sign up to your PlusAuth tenant. Upon successful login or signup, you should be redirected back to the application.