Skip to main content

Add SAML SSO to Express.js App

This guide assumes that you have a Express.js app and want to enable SAML Single Sign-On authentication for your enterprise customers. By the end of this guide, you'll have an app that allows you to authenticate the users using SAML Single Sign-On.

Visit the GitHub repository to see the source code for the Express.js SAML SSO integration.

Integrating SAML SSO into an app involves the following steps.

  • Configure SAML Single Sign-On
  • Authenticate with SAML Single Sign-On

Configure SAML Single Sign-On

This step allows your tenants to configure SAML connections for their users. Read the following guides to understand more about this.

Authenticate with SAML Single Sign-On

Once you add a SAML connection, the app can use this SAML connection to initiate the SSO authentication flow using SAML Jackson. The following sections focuses more on the SSO authentication side.

Install SAML Jackson

To get started with SAML Jackson, use the Node Package Manager to add the package to your project's dependencies.

npm i --save @boxyhq/saml-jackson

Setup SAML Jackson

Setup the SAML Jackson to work with Express.js app.

jackson.js
const baseUrl = 'https://your-app.com';
const samlAudience = 'https://saml.boxyhq.com';
const product = 'saml-demo.boxyhq.com';
const samlPath = '/sso/acs';
const redirectUrl = `${baseUrl}/sso/callback`;

// SAML Jackson options
const options = {
externalUrl: baseUrl,
samlAudience,
samlPath,
db: {
engine: 'sql',
type: 'postgres',
url: 'postgres://postgres:postgres@localhost:5432/postgres',
},
};

module.exports = {
baseUrl,
product,
samlPath,
redirectUrl,
samlAudience,
options,
};

samlPath is where the identity provider POST the SAML response after authenticating the user and redirectUrl is where the SAML Jackson redirects the user after authentication.

Initialize the SAML Jackson as below.

Please note that the initialization of @boxyhq/saml-jackson is async. Therefore, you cannot run it at the top level. Instead, run this in a function where you initialize the express server.

routes/index.js
const { options, product, redirectUrl } = require('../jackson');

let apiController;
let oauthController;

(async function init() {
const jackson = await require('@boxyhq/saml-jackson').controllers(options);

apiController = jackson.connectionAPIController;
oauthController = jackson.oauthController;
})();

Make Authentication Request

Let's add a route to begin the authenticate flow; this route initiates the SAML SSO flow by redirecting the users to their configured Identity Provider.

/routes/index.js
router.post('/sso', async (req, res, next) => {
const tenant = 'boxyhq.com'; // The user's tenant

const { redirect_url } = await oauthController.authorize({
tenant,
product,
state: 'a-random-state-value',
redirect_uri: redirectUrl,
});

res.redirect(redirect_url);
});

Receives SAML Response

After successful authentication, Identity Provider POST the SAML response to the Assertion Consumer Service (ACS) URL.

Let's add a route to handle the SAML response. Ensure the route matches the value of the samlPath you configured while initializing the SAML Jackson library and should be able to receives POST request.

/routes/index.js
router.post('/sso/acs', async (req, res, next) => {
const { RelayState, SAMLResponse } = req.body;

const { redirect_url } = await oauthController.samlResponse({
RelayState,
SAMLResponse,
});

res.redirect(redirect_url);
});

Request Access Token

Let's add another route for receiving the callback after the authentication. Ensure the route matches the value of the redirectUrl you configured previously.

The application requests an access_token by passing the authorization code along with authentication details, including the client_id, client_secret, and redirect_uri.

/routes/index.js
router.get('/sso/callback', async (req, res, next) => {
const { code, state } = req.query;

const tenant = 'boxyhq.com'; // The user's tenant
const product = 'saml-demo.boxyhq.com'; // Your app or product name

const clientId = `tenant=${tenant}&product=${product}`;
const clientSecret = 'dummy';

// Exchange the `code` for `access_token`
const { access_token } = await oauthController.token({
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUrl,
});
});

Fetch User Profile

Once the access_token has been fetched, you can use it to retrieve the user profile from the Identity Provider. The userInfo method returns a response containing the user profile if the authorization is valid.

const user = await oauthController.userInfo(access_token);

The entire response will look something like this:

{
"id":"<id from the Identity Provider>",
"email": "[email protected]",
"firstName": "SAML",
"lastName": "Jackson",
"requested": {
"tenant": "<tenant>",
"product": "<product>",
"client_id": "<client_id>",
"state": "<state>"
},
"raw": {
...
}
}

Authenticate User

Once the user has been retrieved from the Identity Provider, you may determine if the user exists in your application and authenticate the user. If the user does not exist in your application, you will typically create a new record in your database to represent the user.