Win $20,000. Help build the future of education. Answer the call. Learn more

Modern authentication protocols

Authentication is an important part of any web application. It’s often the first interaction a returning user has with your site.

Users expect to be able to use modern methods such as FIDO2 or QR code login for their initial first-factor authentication in addition to traditional passwords. Consumers may prefer to log in using a social provider. Employees expect to log in with their corporate credentials. To protect themselves, all users expect to be able to protect their accounts with 2-factor authentication (2FA).

Implementing and maintaining all these different authentication methods can be a substantial distraction from the real work of writing the core functionality of your application. This tutorial shows how you can offload authentication to an external provider. You implement a simple single sign-on connection and the external provider takes care of everything else.

An external authentication provider

IBM Security Verify is used as the external authentication provider for this tutorial. Follow the steps below to build an example node.js express application which uses the IBM Verify JavaScript SDK to make the required connection using the OpenID Connect (OIDC) single sign-on protocol.

Learn about OpenID Connect

OpenID Connect v1.0 (OIDC) is a modern standard for web single sign-on. It adds an identity layer to the OAuth 2.0 standard. These standards are popular because they have simple client-side implementations, making it easy for you to get connected.

The standards support different grant types for different use cases. For web applications, the authorization code grant type is the most commonly used and most widely supported.

If you’re implementing a native mobile application or you’re building an application on an IoT device, other grant types may be more suitable for your needs.

The end user is re-directed to an OIDC Provider for authentication in the authorization code flow. The user experience and the method by which the end user is authenticated are completely under the control of the provider.

The OIDC Provider generates a short-lived, single-use, authorization code which is returned to your application when authentication is complete. It is this authorization code that gives this grant type its name.

When your application receives the authorization code, it makes a direct connection to the OIDC Provider and exchanges the authorization code for an identity token (and an optional access token).

OIDC authorization code flow

The identity token is in the form of a signed JSON Web Token (JWT). Standard libraries can validate information in and extract information from the token. Alternatively, you can make a connection back to the OIDC Provider and ask for identity information to be provided as a simple JSON object.

Tutorial prerequisites

You need to have the following prerequisites in place before you can start this tutorial.

Node.js

You need to have node.js installed on the system where you will run the example application.

Web browser

You need to have a web browser installed on the system where you run the example application. This is required because the example application listens on localhost (127.0.0.1).

IBM Security Verify

You need to have an IBM Security Verify tenant as the external authentication provider. If you don’t have a tenant, you can sign up for an IBM Security Verify trial. Your new tenant will be available in only a few minutes.

You need to define at least one test user in your IBM Security Verify tenant. You can do this in the Users and groups page of your IBM Security Verify administration console.

You need to define a custom application definition in your IBM Security Verify tenant with the following properties:

  • Sign-on method: OpenID Connect 1.0
  • Enabled grant types: Authorization code
  • Application URL: http://localhost:3000
  • Redirect URL: http://localhost:3000/auth/callback

If you’re new to IBM Security Verify, we suggest you set up the developer portal and then use the developer portal to add the application definition.

Alternatively, you can create the application definition directly in the IBM Security Verify admin interface. If you need help with that, check out the IBM Security Verify product documentation.

Your test user must be authorized to use the application. The easiest way to do this is to select Automatic access for all users and groups under Entitlements in the properties of the custom application.

If you need help with this, check out the IBM Security Verify product documentation.

Set up node.js

Create a project directory and initialize node.js

Create a new directory on your system and then go into that directory. This is your project directory. Run all subsequent commands in this guide in the project directory.

Initialize the project directory for node.js with the following command:

npm init -y

Install the required dependencies

The following dependencies are required for the application:

The following command installs them into the node_modules directory – and adds them to the package.json file which is used to keep track of required packages:

npm install ibm-verify-sdk express express-session dotenv

Create the .env file

An environment file is the recommended way of storing an environment-specific configuration (such as OAuth connection information) for a node.js application. This is a file named .env in your project directory.

Using an environment file means that values are not hard coded into the source. Using the node package dotenv we can load the contents of the environment file into the process.env object.

Create a file named .env in your project directory and paste in the following content:

TENANT_URL=https://your-tenant-id.verify.ibm.com
CLIENT_ID=client-id-from-app-definition
CLIENT_SECRET=client-secret-from-app-definition
APP_URL=http://localhost:3000
RESPONSE_TYPE=code
FLOW_TYPE=authorization
SCOPE=openid
SESSION_SECRET=somethinghardtoguess

You will need to complete the values for TENANT_URL, CLIENT_ID, and CLIENT_SECRET.

The TENANT_URL is the URL of your IBM Security Verify tenant. This will usually have the format: https://xxx.verify.ibm.com.

You will need to get the CLIENT_ID and CLIENT_SECRET from the application definition in IBM Security Verify.

Create the server.js file

Create the file server.js in the project directory. This will be your server application. The sections below describe the content of the file. Copy and paste from each section or use the complete code source3 at the end of this document.

Import the required packages

const express              = require('express');
const session              = require('express-session');
const OAuthContext         = require('ibm-verify-sdk').OAuthContext;

Load the contents of .env into process.env

require('dotenv').config();

Set up the express server

Initialize the express server. Enable sessions. Listen on port 3000.

const app = express();

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true
}));

let port = 3000;
app.listen(port, () => {
    console.log(`Server started.  Listening on port ${port}.`);
})

Instantiate the Verify SDK OAuthContext

The context is generated by building a config object and then passing this to the OAuthContext constructor. Note that most of the configuration is being taken from variables that have been loaded from the environment file.

let config = {
    tenantUrl    : process.env.TENANT_URL,
    clientId     : process.env.CLIENT_ID,
    clientSecret : process.env.CLIENT_SECRET,
    redirectUri  : process.env.APP_URL + "/auth/callback",
    responseType : process.env.RESPONSE_TYPE,
    flowType     : process.env.FLOW_TYPE,
    scope        : process.env.SCOPE
};

let authClient = new OAuthContext(config);

Set up the middleware function to require authentication

This middleware function checks for an authenticated session based on the existence of the token object in the session. If the token object is found, the next() call allows processing to continue.

If the token object is not found, The authenticate function is used to generate an authentication trigger URL and then the browser is redirected to this URL. This passes control to IBM Security Verify so that it can perform authentication. If successful, the browser will then be redirected back to the redirect route below.

The originally requested URL is stored in the session as target_url. The target_url will be used to redirect the user back to the requested target after the authentication flow completes.

async function authentication_required(req, res, next) {
  if (req.session.token) {
    next()
  } else {
    req.session.target_url = req.url;
    try {
      let url = await authClient.authenticate();
      res.redirect(url);
    } catch (error) {
      res.send(error);
    }
    return;
  }
}

OIDC redirect route

This route is called via a redirect after authentication at IBM Security Verify is complete. The getToken function is used to retrieve tokens for the authenticated user from IBM Security Verify. The tokens are stored in the session token object for later use.

After successful completion, the user is redirected to the target_url stored in the session. This is set by the require_authentication function (see above). If a stored URL is not available, the user is redirected to /.

Note: It is useful to have an absolute expiry time for the returned access token. This is calculated from the current time and the expires_in property of the returned token object.

app.get('/auth/callback', async (req, res) => {
  try {
    let token = await authClient.getToken(req.url)
    token.expiry = new Date().getTime() + (token.expires_in * 1000);
    req.session.token = token;
    let target_url = req.session.target_url ? req.session.target_url : "/";
    res.redirect(target_url);
    delete req.session.target_url;
  } catch (error) {
    res.send("ERROR: " + error);
  };
});

Set up the application logout route

This route triggers an application logout. It removes the IBM Security Verify tokens from the session and displays a message in the browser. You could optionally re-direct the browser to trigger a logout at IBM Security Verify at this point if that was required.

app.get('/logout', (req, res) => {
    delete req.session.token;
    res.send("Logged out");
    return;
})

Set up the utility function to process API responses

The Verify SDK returns an object that contains a response object and a token object when calling functions that require a token. If the token object is populated, it is a refreshed token that replaces the one currently held. The response object contains the API response.

This utility function performs the required token check and returns the embedded response object. This function is used in the homepage route to process the response from the userInfo call.

function process_response(response) {
  if (response.token && response.token.expires_in) {
    response.token.expiry = new Date().getTime() + (token.expires_in * 1000);
    req.session.token = response.token;
  }
  return response.response;
}

Set up the application homepage route

This is the application home page. It invokes the authentication_required middleware function to enforce authentication. This means req.session.token will be populated.

Assuming the OIDC scope was included when the OAuthContext was initialized, req.session.token includes an id_token attribute which contains a signed JSON Web Token (JWT). This JWT could be validated and parsed to get user data.

However, rather than doing that here, the userInfo function of the OAuthContext is used to get identity information which is then displayed in the browser. The process_response utility function is used to process the response.

app.get('/', authentication_required, async (req, res) => {
  let userInfo = process_response(await authClient.userInfo(req.session.token));
  res.send(`<h1>Welcome ${userInfo.name}</h1>` +
    `<p>UserID: ${userInfo.preferred_username}</p>`);
});

Start the server and test

Start the server:

node server.js

Navigate to http://localhost:3000.

The authentication_required middleware function determines that no tokens are available, uses the SDK to generate an authentication trigger URL, and re-directs to it.

Assuming the user is not already authenticated, IBM Security Verify displays a login page:

IBM Security Verify default login page

The image above shows the default end user login page for IBM Security Verify. It’s important to note that the authentication methods offered here and the branding of this page are all configurable on a per application basis.

Log in to IBM Security Verify. If login is successful you will be redirected back to the redirect URL of your example application. It will then obtain tokens from IBM Security Verify and redirect the (now authenticated) user back to the homepage.

The homepage route makes a call to the IBM Security Verify userInfo endpoint and uses the information returned to show the name and username of the authenticated user.

Example application homepage showing authenticated user

Navigate to http://localhost:3000/logout to log out.

Congratulations! You have successfully created an application which offloads authentication to an external provider.

Right now you’re only performing username and password authentication but you’re only a few clicks away from adding password-less authentication with FIDO2 login, QR code login, or adding second factor authentication with e-mail, SMS, or voice OTP, Time-based One Time Password (a.k.a Google Authenticator), or Mobile Push verification.

You can learn about setting these up in our IBM Security Verify basics cookbook.

Complete code source

The full source code below includes console.log statements so you can trace the calls being made in the application console.

// Imports
const express = require('express');
const session = require('express-session');
const OAuthContext = require('ibm-verify-sdk').OAuthContext;

// Load contents of .env into process.env
require('dotenv').config();

// Express setup
const app = express();

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true
}));

let port = 3000;
app.listen(port, () => {
  console.log(`Server started.  Listening on port ${port}.`);
})

// Instantiate OAuthContext
let config = {
  tenantUrl: process.env.TENANT_URL,
  clientId: process.env.CLIENT_ID,
  clientSecret: process.env.CLIENT_SECRET,
  redirectUri: process.env.APP_URL + "/auth/callback",
  responseType: process.env.RESPONSE_TYPE,
  flowType: process.env.FLOW_TYPE,
  scope: process.env.SCOPE
};

let authClient = new OAuthContext(config);

// Middleware function to require authentication
// If token object found, pass to next function.
// If no token object found, generate OIDC
// authentication request and redirect user
async function authentication_required(req, res, next) {
  if (req.session.token) {
    next()
  } else {
    req.session.target_url = req.url;
    try {
      let url = await authClient.authenticate();
      console.log("** Calling: " + url);
      res.redirect(url);
    } catch (error) {
      res.send(error);
    }
    return;
  }
}

// OIDC redirect route
// user has authenticated through SV, now get the token
app.get('/auth/callback', async (req, res) => {
  try {
    console.log("** Response: " + req.url);
    console.log("** Calling token endpoint");
    let token = await authClient.getToken(req.url)
    console.log("** Response: " + JSON.stringify(token));
    token.expiry = new Date().getTime() + (token.expires_in * 1000);
    req.session.token = token;
    let target_url = req.session.target_url ? req.session.target_url : "/";
    res.redirect(target_url);
    delete req.session.target_url;
  } catch (error) {
    res.send("ERROR: " + error);
  };
});

// Logout route
app.get('/logout', (req, res) => {
  delete req.session.token;
  res.send("Logged out");
  return;
})

// Utility function to parse API responses
// Checks and processes any refreshed token
// Returns enclosed API response.
function process_response(response) {
  if (response.token && response.token.expires_in) {
    console.log("** Refreshed token: " + JSON.stringify(response.token));
    response.token.expiry = new Date().getTime() + (token.expires_in * 1000);
    req.session.token = response.token;
  }
  return response.response;
}

// Home route - requires authentication
// Uses userInfo to get user information JSON from Verify
app.get('/', authentication_required, async (req, res) => {
  console.log("** Calling userInfo");
  let userInfo = process_response(await authClient.userInfo(req.session.token));
  console.log("** Response:" + JSON.stringify(userInfo));
  res.send(`<h1>Welcome ${userInfo.name}</h1>` +
    `<p>UserID: ${userInfo.preferred_username}</p>`);
});