Adding SAML Single Sign-On to an Express App: A Step-by-Step Guide πŸš€πŸš€

devkiran

Kiran Krishnan

Posted on September 4, 2023

Adding SAML Single Sign-On to an Express App: A Step-by-Step Guide πŸš€πŸš€

In this article, you'll learn how add SAML SSO login to an Express.js app. You'll use SAML Jackson with Auth0 to authenticate users and protect routes.

GitHub logo boxyhq / jackson

πŸ”₯ Streamline your web application's authentication with Jackson, an SSO service supporting SAML and OpenID Connect protocols. Beyond enterprise-grade Single Sign-On, it also supports Directory Sync via the SCIM 2.0 protocol for automatic user and group provisioning/de-provisioning. 🀩

SAML Jackson: Open Source Enterprise SSO And Directory Sync

OpenSSF Best Practices Badge NPM downloads badge Docker pull statistics badge Apache 2.0 license badge Open Github issues badge Github stargazers Nodejs version support badge Swagger Validator badge

SAML Jackson bridges or proxies a SAML login flow to OAuth 2.0 or OpenID Connect, abstracting away all the complexities of the SAML protocol. It also supports Directory Sync via the SCIM 2.0 protocol for automatic user and group provisioning/de-provisioning.

We now also support OpenID Connect providers.

A quick demo of the admin portal without sound to show an overview of what to expect. It shows features such as SSO, the ability to set up SSO connections, Setup Links, Directory sync, and more

Directory Sync

SAML Jackson also supports Directory Sync based on the SCIM 2.0 protocol.

Directory sync helps organizations automate the provisioning and de-provisioning of their users. As a result, it streamlines the user lifecycle management process by saving valuable organizational hours, creating a single truth source of the user identity data, and facilitating them to keep the data secure.

For complete documentation, visit boxyhq.com/docs/directory-sync/overview

🌟 Why star this repository?

If you find this project helpful, please consider supporting us by starring the repository and sharing it with others. This helps others find the project…

You can also access the full code at the GitHub repository.

Let’s get started!

Prerequisites

To follow along with this article, you’ll need the following:

  • Node.js installed on your computer
  • Basic knowledge about Node.js and Express.js

Setting up the database

Make sure you have a PostgreSQL database ready. You can use any PostgreSQL database you want.

If you're on macOS, you can use DBngin to create a PostgreSQL database. It's free and easy to use.

Configure the Identity Provider

We'll use the Auth0 as our identity provider. An Identity Provider (IdP) is a service that manage user accounts for your app.

  • First, go to the Auth0 signup page, then create an account.
  • Go to Dashboard > Applications > Applications.
  • Click the Create Application button.
  • Give your new application a name.
  • Choose Regular Web Applications as an application type and the click Create.
  • Go to the app you created, then click the Addons tab.
  • In the SAML2 Web App box, click the slider to enable the Addon.
  • Go to the Usage tab and download the Identity Provider Metadata.
  • Go to the Settings tab and make below changes.
  • Add http://localhost:3000/sso/acs as your Application Callback URL that receives the SAML response.
  • Paste the following JSON for Settings, then click Enable button.
{
  "audience": "https://saml.boxyhq.com",
  "mappings": {
    "id": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
    "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    "firstName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
    "lastName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
  }
}
Enter fullscreen mode Exit fullscreen mode

audience is just an identifier to validate the SAML audience. More info.

Auth0 provides database connections to authenticate users with an email/username and password. These credentials are securely stored in the Auth0 user store.

Let's create one so that our users can register or login.

Now we've everything ready, let's move to the next step.

Getting started

Launch a terminal and clone the GitHub repo:

git clone https://github.com/devkiran/express-saml.git
Enter fullscreen mode Exit fullscreen mode
cd express-saml
Enter fullscreen mode Exit fullscreen mode

Now, install the dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Add the environment variables:

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Update the DATABASE_URL variable with your Postgres database connection URI.

About the Express app

This is a simple express.js app created using express-generator. You can use any express.js app if you want.

Our express.js app has only 2 routes.

  • GET / render a home page
  • GET /dashboard render a dashboard

So, what's the plan? We'll add SAML SSO login (via Auth0) to our express.js app so that only authenticated users can access the /dashboard.

Install SAML Jackson

Run the following command to install the latest version of the SAML Jackson.

npm i --save @boxyhq/saml-jackson
Enter fullscreen mode Exit fullscreen mode

Once you installed Jackson, let's initialize it.

Add the following code to the routes/index.js.

// routes/index.js

...

let apiController;
let oauthController;

const jacksonOptions = {
  externalUrl: process.env.APP_URL,
  samlAudience: process.env.SAML_AUDIENCE,
  samlPath: '/sso/acs',
  db: {
    engine: 'sql',
    type: 'postgres',
    url: process.env.DATABASE_URL,
  },
};

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

  apiController = jackson.apiController;
  oauthController = jackson.oauthController;
})();
Enter fullscreen mode Exit fullscreen mode

Setting up Express.js routes

Now let's add the routes to our express.js app.

Add SAML Metadata

The first route you'll create is the GET /connection one. This route will display a form with following fields:

  • Metadata: Enter the XML Metadata content you've downloaded from IdP.
  • Tenant: Jackson supports a multi-tenant architecture, this is a unique identifier you set from your side that relates back to your customer's tenant. This is normally an email, domain, an account id, or user-id.
  • Product: Jackson support multiple products, this is a unique identifier you set from your side that relates back to the product your customer is using.
// routes/index.js

router.get('/connection', async (req, res) => {
  res.render('connection');
});
Enter fullscreen mode Exit fullscreen mode

Add a view to display the form.

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

<!DOCTYPE html>
<html>
  <head>
    <title>SAML Connection</title>
    <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp"></script>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>
  <body>
    <div class="max-w-md mx-auto mt-6 p-6 bg-white rounded-lg shadow-md">
      <h1 class="text-2xl font-semibold text-center mb-4">SAML Connection</h1>
      <p class="text-gray-600 text-center mb-4">Add SAML Metadata.</p>

      <form action="/connection" method="POST">
        <div class="mb-4">
          <label for="tenant" class="block text-gray-700">Tenant</label>
          <input
            type="text"
            name="tenant"
            id="tenant"
            class="form-input mt-1 block w-full"
            required="required"
          />
        </div>
        <div class="mb-4">
          <label for="product" class="block text-gray-700">Product</label>
          <input
            type="text"
            name="product"
            id="product"
            class="form-input mt-1 block w-full"
            required="required"
          />
        </div>
        <div class="mb-4">
          <label for="rawMetadata" class="block text-gray-700"
            >Metadata (Raw XML)</label
          >
          <textarea
            name="rawMetadata"
            id="rawMetadata"
            cols="30"
            rows="10"
            class="form-textarea mt-1 block w-full"
            required="required"
          ></textarea>
        </div>
        <div class="mb-4">
          <button
            type="submit"
            class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          >
            Submit
          </button>
        </div>
      </form>
    </div>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Now let's add another route POST /connection that will store the form data by calling the SAML Jackson config API.

This step is the equivalent of setting an OAuth 2.0 app and generating a client ID and client secret that will be used in the login flow.

// routes/index.js

router.post('/connection', async (req, res, next) => {
  const { rawMetadata, tenant, product } = req.body;

  const defaultRedirectUrl = 'http://localhost:3000/sso/callback';
  const redirectUrl = '["http://localhost:3000/*"]';

  try {
    await apiController.config({
      rawMetadata,
      tenant,
      product,
      defaultRedirectUrl,
      redirectUrl,
    });

    res.redirect('/connection');
  } catch (err) {
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

There are a few important things to note in the code above.

defaultRedirectUrl holds the redirect URL to use in the IdP login flow. Jackson will call this URL after completing an IdP login flow.

redirectUrl holds an array containing a list of allowed redirect URLs. Jackson will disallow any redirects that are not on this list.

Next, let's start the express app. The app starts a server and listens on port 3000 (by default) for connections.

npm start
Enter fullscreen mode Exit fullscreen mode

Now, let's visit http://localhost:3000/connection, you should see the page with a form.

Here you can add the metadata you've downloaded from Auth0. Fill out the form with a Tenant, Product, and paste the metadata XML content as it is.

I'll use boxyhq.com as tenant and crm as product.

The response returns a JSON with client_id and client_secret that can be stored against your tenant and product for a more secure OAuth 2.0 flow.

If you do not want to store the client_id and client_secret you can alternatively use client_id=tenant=<tenantID>&product=<productID> and any arbitrary value for client_secret when setting up the OAuth 2.0 flow.

Redirect the users to IdP

Now you have added the SAML metadata, you'll need a route to redirect the users to IdP to start the SAML authentication.

Let's add a new route GET /sso/authorize.

Don't forget to change the values of the tenant and product in the code.

// routes/index.js

router.get('/sso/authorize', async (req, res, next) => {
  try {
    const tenant = 'boxyhq.com';
    const product = 'crm';

    const body = {
      response_type: 'code',
      client_id: `tenant=${tenant}&product=${product}`,
      redirect_uri: 'http://localhost:3000/sso/callback',
      state: 'a-random-state-value',
    };

    const { redirect_url } = await oauthController.authorize(body);

    res.redirect(redirect_url);
  } catch (err) {
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

oauthController.authorize() will returns a redirect_url. You should redirect the users to this redirect_url to start the IdP authentication flow.

Handle the SAML Response from IdP

This route becomes the Assertion Consumer Service (ACS) URL of your app. The ACS URL tells your IdP where to POST its SAML Response after authenticating a user.

The SAML Response contains 2 fields: SAMLResponse and RelayState.

// routes/index.js

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

    const body = {
      SAMLResponse,
      RelayState,
    };

    const { redirect_url } = await oauthController.samlResponse(body);

    res.redirect(redirect_url);
  } catch (err) {
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

Call to the method oauthController.samlResponse() will returns a redirect_url. You should redirect the users to this redirect_url. The query parameters will include the code and state parameters.

Code exchange

Now exchange the code for a token. The token is required to access the user profile.

Let's create a new route GET /sso/callback to handle the callback.

// routes/index.js

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

  const tenant = 'boxyhq.com';
  const product = 'crm';

  const body = {
    code,
    client_id: `tenant=${tenant}&product=${product}`,
    client_secret: 'dummy',
  };

  try {
    // Get the access token
    const { access_token } = await oauthController.token(body);

    // Get the user information
    const profile = await oauthController.userInfo(access_token);

    // Add the profile to the express session
    req.session.profile = profile;

    res.redirect('/dashboard');
  } catch (err) {
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

In the above code, replace the value for tenant and product with yours.

Protect the dashboard

Now is the time to fix our GET /dashboard route so that only authenticated users can access it.

Let's fix it by adding a condition to check if the profile exists in the session.

If profile is undefined, redirect the users back to the / otherwise display the profile on the dashboard.

Replace the GET /dashboard route with the below code.

// routes/index.js

router.get('/dashboard', function (req, res, next) {
  const { profile } = req.session;

  if (profile === undefined) {
    return res.redirect('/');
  }

  // Pass the profile to the view
  res.render('dashboard', {
    profile,
  });
});
Enter fullscreen mode Exit fullscreen mode

Replace the views/dashboard.ejs view with the below code.

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

<!DOCTYPE html>
<html>
  <head>
    <title>Dashboard</title>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>
  <body>
    <h1>Dashboard</h1>
    <p>Only authenticated users should access this page.</p>

    <p>Id - <%= profile.id %></p>
    <p>Email - <%= profile.email %></p>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

From the command line, let's restart the express app then visit the authorize the URL http://localhost:3000/sso/authorize.

If you've configured everything okay, it should redirect you to the Auth0 authentication page, then click on the Sign up link and register there

If the authentication is successful, the app will redirect you to the dashboard and display the id, email of the user.

Conclusion

Congratulations, you should now have a functioning SAML SSO integrated with your express.js app using the SAML Jackson and Auth0.

References

To learn more about SAML Jackson, take a look at the following resources:

Your feedback and contributions are welcome!

πŸ’– πŸ’ͺ πŸ™… 🚩
devkiran
Kiran Krishnan

Posted on September 4, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related