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.
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
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.
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"
}
}
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.
- Go to Auth0 Dashboard > Authentication > Database.
- Click Create DB Connection - Auth0 Create DB Document
- Give your connection a name, then click Create.
- Go to the Applications tab and enable the application you just created.
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
cd express-saml
Now, install the dependencies:
npm install
Add the environment variables:
cp .env.example .env
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
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;
})();
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');
});
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>
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);
}
});
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
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);
}
});
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);
}
});
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);
}
});
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,
});
});
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>
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!
Top comments (2)
Nice article @devkiran!
Thank you @nathan_tarbert