Adding SAML Single Sign-On to an Express App: A Step-by-Step Guide ππ
Kiran Krishnan
Posted on September 4, 2023
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.
SAML Jackson: Open Source Enterprise SSO And Directory Sync
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.
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.
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β¦
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.
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.
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.
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.
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.jsrouter.get('/sso/callback',async (req,res,next)=>{const{code}=req.query;consttenant='boxyhq.com';constproduct='crm';constbody={code,client_id:`tenant=${tenant}&product=${product}`,client_secret:'dummy',};try{// Get the access tokenconst{access_token}=awaitoauthController.token(body);// Get the user informationconstprofile=awaitoauthController.userInfo(access_token);// Add the profile to the express sessionreq.session.profile=profile;res.redirect('/dashboard');}catch (err){next(err);}});
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.jsrouter.get('/dashboard',function (req,res,next){const{profile}=req.session;if (profile===undefined){returnres.redirect('/');}// Pass the profile to the viewres.render('dashboard',{profile,});});
Replace the views/dashboard.ejs view with the below code.
<!-- views/dashboard.ejs --><!DOCTYPE html><html><head><title>Dashboard</title><linkrel="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>