Use OpenID Connect to Build a Simple Node.js Website

Use OpenID Connect to Build a Simple Node.js Website

If you’ve ever spent time trying to figure out the best way to handle user authentication for your Node app and been confused: you’re not alone. Over the last few years, authentication practices have changed quite a bit.

Today I’m going to show you how to use OpenID Connect to build an extremely simple Node.js website (using Express.js) that allows you to manage your users, log them in, and log them out.

Websites used to require users to register with a username/password and log in with those same credentials. This method was simple but caused many security problems because developers would need to write the code to authenticate the user directly, store their credentials, manage their data, etc. This practice also required developers to build custom authorization schemes to track the user permissions necessary to perform certain operations.

A while later, OAuth came into fashion with a new idea: let a user have one account with a large OAuth provider (Google, Facebook, etc.) and allow users to log in to your service via their OAuth account with that provider. This method had some excellent benefits: developers no longer needed to worry about storing passwords and managing credentials. The downside was that OAuth is a flexible protocol and doesn’t lay out rules around authorization, data management, etc. This means that developers using pure OAuth must write a lot of custom security code themselves, which causes problems.

Then, OpenID Connect (OIDC) came onto the scene. It’s a protocol built on top of OAuth that provides everything you could ever want: simplified user authentication, simplified authorization, and lots of nice management to tie them all together. OIDC has been gaining popularity in the development community.

Without further ado, let’s build something together! I’ll show you how to use the oidc-middleware package to make adding user authentication and authorization to your Node apps simple.

Table of Contents

Prerequisites

This tutorial uses the following technologies but doesn’t require any prior experience:

Initialize authentication with Okta

Dealing with user authentication in web apps can be a massive pain for every developer. This is where Okta shines: it helps you secure your web applications with minimal effort.

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Other. Then, change the Redirect URI to http://localhost:3000/authorization-code/callback and use http://localhost:3000/ for the Logout Redirect URI.

What does the Okta CLI do?

The Okta CLI will create an OIDC Web App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. You will see output like the following when it’s finished:

Okta application configuration has been written to: /path/to/app/.okta.env

Run cat .okta.env (or type .okta.env on Windows) to see the issuer and credentials for your app.

export OKTA_OAUTH2_ISSUER="https://dev-133337.okta.com/oauth2/default"
export OKTA_OAUTH2_CLIENT_ID="0oab8eb55Kb9jdMIr5d6"
export OKTA_OAUTH2_CLIENT_SECRET="NEVER-SHOW-SECRETS"

Your Okta domain is the first part of your issuer, before /oauth2/default.

NOTE: You can also use the Okta Admin Console to create your app. See Create a Web App for more information.

NOTE: If you’d like to skip the following sections and get straight into the code, you can visit the GitHub repo for this application directly.

Build the Express.js App

Next, you’ll build a simple Express.js app without any login capabilities. It will be straightforward (but that’s the point!).

Create the Application Skeleton

Create a new folder somewhere on your computer and enter it to get started. Then create a server.js file and insert the following code:

"use strict";

const express = require("express");

let app = express();

// App settings
app.set("view engine", "pug");

// App middleware
app.use("/static", express.static("static"));

// App routes
app.get("/", (req, res) => {
  res.render("index");
});

app.get("/dashboard", (req, res) => {
  res.render("dashboard");
});

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

app.listen(3000);

This is a basic Express.js application:

  • It creates an Express application
  • It configures Express to serve static files (CSS, images, etc.)
  • It contains three routes: a home page route, a dashboard route, and a logout route. The home page route shows an HTML template (that we’ll create in a moment). The dashboard route shows a dashboard template. And the logout route redirects the user back to the home page.
  • On the very last line of the file, Express will start up a local web server on port 3000 so you can view the website locally.

Next, you’ll need to create a new directory called views, and inside it, create a base.pug template with the following contents:

doctype html
html(lang="en")
  head
    meta(charset="utf-8")
    meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no")

    link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css", integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3", crossorigin="anonymous")
    link(rel="stylesheet", href="/static/css/style.css")

  body
    .container
      block body

    script(src="https://code.jquery.com/jquery-3.6.0.slim.min.js", integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=", crossorigin="anonymous")
    script(src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js", integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p", crossorigin="anonymous")

This is a simple pug template that contains nothing more than some very basic HTML formatting, and Bootstrap. If you aren’t already familiar with pug, you may want to read through this excellent beginner’s tutorial.

NOTE: pug used to be named jade. This is useful to know if you’re looking for resources online.

Next, you’ll want to create the HTML template that renders the site’s home page. Create the file views/index.pug and include the following code:

extends base.pug

block body
  h1.text-center Welcome to the Example App!

  .jumbotron
    p.
      Thanks for checking out this super simple Okta sample app.  If you login
      below, you'll be taken to an admin panel that is only accessible for
      authenticated users.

    p.
      Please #[a(href="/login") login] to continue.

This is your basic home page template.

Now, let’s create a dashboard page. This page will be what the user sees after logging into the website. Create the file views/dashboard.pug and include the following:

extends base.pug

block body
  h1.text-center User Dashboard

  .jumbotron
    ul
      li Your Email Address is: #{user.preferred_username}
      li Your First Name is: #{user.given_name}
      li Your Last Name is: #{user.family_name}

    p.
      If you'd like to logout, please #[a(href="/logout") click here].

Next, let’s add a bit of CSS to make things look nice. Create a new folder to store your static assets (CSS, images, etc.):

mkdir -p static/css

Now create the file static/css/style.css and include the following:

h1 {
  margin-top: 2em;
}

.jumbotron {
  margin-top: 2em;
  padding: 2rem 1rem;
  margin-bottom: 2rem;
  background-color: #e9ecef;
  border-radius: .3rem;
}

Now, if you want to run this simple website, you can do so by installing the required dependencies, then starting up your Node server on the command line:

npm install express@4 pug@3
node server.js

Once the server is running, you can view the site by visiting http://localhost:3000 in your browser.

Remember how I said this would be a simple website? I wasn’t lying! Here’s what your new website’s homepage will look like:

Application Homepage

By now, you should have a working website with no authentication. So let’s take it one step further in the next section and add OIDC.

Add OpenID Connect to Your Website

To get started, you’ll need to install two new Node.js libraries:

  • express-session, which will manage user sessions for your website
  • oidc-middleware, which will handle all of the OIDC implementation details for your website

To install these libraries, run the following command:

npm install express-session@1 @okta/oidc-middleware@4

Next, you’ll need to import these libraries in your server.js from before:

const express = require("express");
const session = require("express-session");
const { ExpressOIDC } = require("@okta/oidc-middleware");

Now that the libraries have been imported, you can initialize the session middleware and the OIDC middleware:

// App middleware
app.use("/static", express.static("static"));

app.use(session({
  cookie: { httpOnly: true },
  secret: "can you look the other way while I type this"
}));

let oidc = new ExpressOIDC({
  issuer: "https://{yourOktaDomain}/oauth2/default",
  client_id: "{clientId}",
  client_secret: "{clientSecret}",
  appBaseUrl: "http://localhost:3000",
  routes: {
    loginCallback: {
      afterCallback: "/dashboard"
    }
  },
  scope: 'openid profile'
});

The session middleware contains a number of options, but the only ones we’ll need for now are the following two:

  • cookie.httponly: this option tells the browser that JavaScript code should not be allowed to access the session data. JavaScript on clients is a dangerous thing. It’s essential to ensure the safety of cookies that contain identity information.
  • secret: this option should be a long random string you create. It should be the same across all your webservers but never shared publicly or stored in a public place. This value is used to ensure your user’s identity information is protected cryptographically inside of cookies.

The OIDC middleware also contains a number of options. I’ll walk you through them briefly:

  • issuer: this should be your Org URL value (that you wrote down earlier) with /oauth2/default appended. This is the OAuth2 endpoint that’s used for handling authorization.
  • client_id/client_secret: these values are what you wrote down earlier after creating your Okta Application.
  • appBaseUrl: the base scheme, host, and port (if not 80/443) of your app, not including any path (e.g. http://localhost:3000, not http://localhost:3000/ )
  • routes.loginCallback.afterCallback: this option tells Okta where to redirect a user once they’ve been signed into your website. In this case, you’ll want to redirect them to the dashboard page.
  • scope: the OpenID Connect protocol has a lot of standard scopes that determine what data about your user is returned to you once the user has been signed in. The values here provide basic user information for your website. To view a complete list of available scopes, check out this page.

Now that you’ve configured OIDC for your website, it’s time to hook up the routes:

// App routes
app.use(oidc.router);

app.get("/", (req, res) => {
  res.render("index");
});

app.get("/dashboard", oidc.ensureAuthenticated(), (req, res) => {
  console.log(req.userContext.userinfo);
  res.render("dashboard", { user: req.userContext.userinfo });
});

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

The first thing that’s happening above is that you’re using the built-in OIDC routes that ship with the oidc-middleware library. This library provides routes to handle authenticating the user correctly (behind the scenes) and many other things. I’ll show you how these work soon.

You’ll also notice that your dashboard route is now using a new Node.js middleware: oidc.ensureAuthenticated(). This middleware will do the following:

  • If a user tries to visit /dashboard and is not logged in, they will be redirected to Okta to log in before being allowed to visit the page
  • If a user tries to visit /dashboard and they are logged in, they will be allowed to view the page with no problems

You’ll also notice that you’re now able to access the logged-in user’s personal information via req.userContext.userinfo inside the dashboard route. The oidc-middleware library makes this object available to you whenever a user is logged in. You’ll notice that this object shows the following information:

{ 
  sub: '00uc5nynm5RZivEun0h7',
  name: 'Randall Degges',
  locale: 'en-US',
  preferred_username: 'r@rdegges.com',
  given_name: 'Randall',
  family_name: 'Degges',
  zoneinfo: 'America/Los_Angeles',
  updated_at: 1507772025 
}

The data that’s returned about each logged-in user can be modified by including more (or fewer) scopes (as mentioned previously).

In the code above, you’ll also notice an actual logout implementation. The oidc-middleware library includes a new method req.logout(), which wipes all session data and logs the user out of your application.

Modify the Server Start

Now that you’ve got your code in place, there’s only one tiny piece of code left to change: the code that starts your web server.

Usually, when your Node application starts running, via the app.listen() method, the website is immediately online. However, now that you’re using OIDC, you don’t want that behavior.

To set up the OIDC rules and policies, the oidc-middleware library performs its setup routines asynchronously. If your site was to go online immediately, it could cause errors when users try to view protected pages, etc.

To get around this, you’ll want to modify your server start code like so:

oidc.on("ready", () => {
  app.listen(3000);
});

oidc.on("error", err => {
  console.error(err);
});

By listening for the events that the oidc-middleware library provides, you can safely start your Node server as soon as the OIDC setup has finished, thereby solving any timing problems, you might have run into otherwise.

Test It Out

Now that your application has been built, why not try it out? If you visit http://localhost:3000 and click through the prompts, you’ll see how everything fits together:

Once you click login, you’ll be redirected to /login The oidc-middleware will intercept that /login request, and redirect the user to Okta’s hosted sign-in page where they’ll be prompted for their email address and password. The user will then enter their credentials and log in. They will then be redirected back to your local website, where the oidc-middleware library will again intercept the request, create a session for the user, and log them in. Finally, they will be redirected to the dashboard page (/dashboard), where your route code will run and echo their basic information back to them.

Here’s what each of the pages looks like in the flow:

Application Homepage

Then, once you click “login”, you’ll be taken to the login page:

Application Login Page

Finally, once you’ve logged in, you’ll be taken to the dashboard page where you can view your user information:

Application Dashboard

Resources

Now that you’ve built your first Node.js site using OIDC to handle authentication using our oidc-middleware library, you might want to learn more about OIDC.

One of my good friends and co-workers Micah Silverman recently published a three part primer to OIDC which I strongly recommend you read if you’re interested in learning more about OIDC. You can check it out here.

You can also follow OktaDev and Okta on Twitter to see more of what we’re working on at Okta or follow Randall for my latest work. on, and ask any auth-related questions you might have.

Changelog:

Randall Degges runs Evangelism at Okta where he works on security research, development, and education. In his spare time, Randall writes articles and gives talks advocating for security best practices. Randall also builds and contributes to various open-source security tools.

Randall's realm of expertise include Python, JavaScript, and Go development, web security, cryptography, and infrastructure security. Randall has been writing software for ~20 years and has built some of the most-used API services on the internet.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.