Single Sign-on
Signal Sign-on

SSO Login in Node.js made easy: A Comprehensive Guide to User Authentication with SAML and Leading IdPs like Okta, Auth0, and More

Mujtaba Hassan
6 min readAug 20, 2024

--

In this article, you’ll learn how to integrate any Identity Provider (IdP) with your Node.js application. Unlike many guides that focus on a specific IdP, we’ll develop a generic approach that enables your app to work with any IdP. This guide is particularly useful for multi-tenant applications, where your app is designed to serve multiple organizations or clients, each potentially using a different Identity Provider (IdP). While we’ll use Okta as our example, the same principles can be applied to integrate other IdPs seamlessly.

I’ll assume you’re somewhat familiar with Node.js, Single Sign-On (SSO), and SAML. To briefly recap, SSO is an authentication process that allows users to access multiple applications or services with a single set of credentials, with SAML serving as the standard protocol that securely exchanges authentication information. Once authenticated, users can seamlessly navigate between services without needing to log in again. Below are the straightforward steps to set up Okta integration with your Node.js app.

Set up an okta account and application

Go to https://developer.okta.com/ and register a developer account.

After registering your account go to applications and select create App Integration.

Select SAML 2.0 as the sign-in method.

Configure Application

Since we are doing this for learning purposes, we’ll use the local Node.js app URL http://localhost:4000. The Single Sign-On URL is set to http://localhost:4000/auth/sso-callback?domainName=test_org, where Okta sends the SAML assertion after login, the domainName is included in the URL to identify which organization's SSO configuration should be used during the callback. The Audience URI (SP Entity ID) is also set to http://localhost:4000, acting as a unique identifier to ensure the SAML response is intended for your application. The Default RelayState is left blank, so users are redirected to the default landing page after login. These settings ensure that the SAML assertion is securely delivered and properly handled by your application.

What Are SAML Assertions?

SAML assertions are XML documents that contain the authentication information about a user, issued by the Identity Provider (IdP) after successful login. These assertions include details like the user’s identity and are sent to the Service Provider (SP), in this case, your Node.js app, to confirm that the user is authenticated. The SP then validates the assertion, ensuring it’s intended for them and proceeds with granting access to the user.

Once you’ve completed the feedback step, navigate to the Sign On tab. Here, you’ll find the settings needed for our integration. We’ll be using the @node-saml/node-saml package to implement our solution. The key parameters you’ll need are:

  • issuer: The unique identifier for the Identity Provider (Okta), ensuring the SAML assertion is issued by the correct IdP.
  • audience: The intended recipient of the SAML assertion, matching your application’s SP Entity ID, to confirm it’s meant for your Node.js app.
  • entryPoint (sign on URL): The URL where the SAML authentication request is sent, directing users to the IdP’s login page (Okta) to initiate SSO.
  • callbackUrl: The endpoint in your app where Okta sends the SAML response after login, handling and validating the assertion.
  • wantAuthnResponseSigned: Specifies whether the SAML response should be signed by the IdP. Set to false here, meaning a signed response isn’t required.
  • idpCert: The public certificate from Okta used to verify the authenticity and integrity of the SAML response.
{
"issuer": "http://www.okta.com/xxxxxxxxxx",
"audience": "http://localhost:4000",
"entryPoint": "https://dev-xxxxxxx.okta.com/app/xxxxxxx/xxxxxxxxx/sso/saml",
"callbackUrl": "http://localhost:4000/auth/sso-callback",
"wantAuthnResponseSigned": false,
"idpCert": "xxxxx"
}

You can get the above required parameters from the sign on tab.

Assign people to the app.

Set up the node.js application

Next, set up a basic Node.js application or integrate this into your existing application. You’ll need to add two new endpoints:

  1. /sso-login (GET): This endpoint initiates the SSO process. It retrieves the organization's SSO configuration based on the provided domainName and generates a login URL, which is then sent back to the client.
  2. /sso-callback (POST): This endpoint handles the SSO callback. After the user successfully logs in via the Identity Provider (IdP), the SAML response is sent to this endpoint. It validates the SAML assertion, verifies the user, generates an access token, and then redirects the user to the dashboard with the token.

Here how the code for these endpoints look:

const ssoLogin = async (req, res)=>{
let { domainName } = req.query;
const organization = await organizations.findOne({domainName})

if(!organization) return res.status(404).json({ message: "No organization found" });
const { ssoConfig } = organization;
const loginUrl = await getLoginUrl(ssoConfig);
res.json({ ssoUrl: loginUrl });
}

async function ssoCallback(req, res) {
try {
let { domainName } = req.query;
const organization = await organizations.findOne({domainName})

if(!organization) throw boom.notFound('Organization Not found');
const assertion = await validateLogin(req.body, organization.ssoConfig);
const email = assertion.nameID
if (!email) throw boom.unauthorized('Email not found in SAML assertion.');
const ssoUser = await user.findOne({ email });

if (!ssoUser) { throw boom.unauthorized('SSO login was attempt was failed.') }
const userAccessToken = generateAccessToken(ssoUser._id.toString());
//redirect to dashboard with the token in the url.
res.redirect(`${process.env.DASHBOARD_URL}?token=${userAccessToken}`);
} catch (error) {
res.status(500).json({ message: error.message });
}
}

Here are utils functions being used:

import { SAML } from "@node-saml/node-saml";
import "dotenv/config";
import jwt from 'jsonwebtoken';

async function getLoginUrl(samlOptions) {
const saml = new SAML(samlOptions);
const loginUrl = await saml.getAuthorizeUrlAsync();
return loginUrl;
};

async function validateLogin(samlResponse, samlOptions) {
const saml = new SAML(samlOptions);
const response = await saml.validatePostResponseAsync(samlResponse);
return response.profile;
};

const generateAccessToken = id=> jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '24h' });

In the above code the SAML class acts as a toolkit for handling SAML requests and responses in Single Sign-On (SSO). By passing samlOptions, you configure this toolkit with the necessary settings to interact with the Identity Provider (IdP) like Okta. The getAuthorizeUrlAsync method generates a secure login URL that directs the user to the IdP’s login page. After the user logs in, the validatePostResponseAsync method uses the toolkit to validate the SAML response (samlResponse) received from the IdP, ensuring the response is legitimate and extracting the user’s information.

Workflow Overview

When you navigate to http://localhost:4000/auth/sso-login?domainName=test_org, the application retrieves the corresponding SSO configuration for the organization based on the domain name provided. The server then generates a SAML authentication request and returns an ssoUrl. The frontend uses this URL to redirect the user to the organization’s Identity Provider (IdP), such as Okta, where they enter their credentials.

After a successful login, Okta will redirect the user to the Single Sign-On URL you configured, which in this case is http://localhost:4000/auth/sso-callback?domainName=test_org. The application will then validate the SAML response, generate a JWT token, and redirect the user to the dashboard URL, in this case http://localhost:3000, with the token included in the URL. The frontend can now use this JWT token to authenticate the user and maintain their session.

Git repo:

If you 🫵 found the article helpful, 👏 and if you think the code was helpful too, a ⭐️ will be appreciated.

--

--