Setting up a Single Sign-On (SSO) SAML Test Environment with NX Workspace, Express.js, Passport, @node-saml/passport-saml, and TypeScript: A Guide with SimpleSAMLphp and a Working Repository

Joshua Wright
12 min readJan 14, 2024

--

TLDR: For those who prefer diving straight into the code, you can access the complete repository here: Github Repository.

A picture of scrabble tiles that spells login. Photo by Miguel Á. Padriñán from Pexels: https://www.pexels.com/photo/close-up-shot-of-keyboard-buttons-2882566/
Photo by Miguel Á. Padriñán from Pexels: https://www.pexels.com/photo/close-up-shot-of-keyboard-buttons-2882566/

Welcome to this unofficial follow-up article, inspired by the valuable work of Jeffry Houser and the Disney Polaris team. In their article, “ Setup a Single Sign On SAML Test Environment with Docker and NodeJS,” they laid the groundwork for a Single Sign-On (SSO) SAML test environment.

I’m Joshua Wright, lead developer at McClelland Laboratories. My professional journey revolves around the development of technology for the mining industry, employing technologies such as Angular, MongoDB, the ExcelJS API, and NestJS. On the weekends, I delve into freelance work, crafting applications that aid graduate students with their thesis projects.

During a recent university related project, the need arose to integrate with a Single Sign-On (SSO) system employing SAML for authentication. The task of setting up a development environment for SAML testing proved to be far from straightforward. It was during this quest that I came across an invaluable article authored by Jeffry Houser, titled “Setup a Single Sign On SAML Test Environment with Docker and NodeJS.” However, my journey soon encountered hurdles; the passport-saml library had become deprecated, and despite following the step by step guide, I had to spend a significant amount of time debugging the code.

In response to these challenges, this article aims to provide both a guide and a code repository, building upon the foundations laid by the original work. Together, we’ll embark on a journey to create a SAML SSO test environment, using NX Workspace, Express.js, Passport, @node-saml/passport-saml, TypeScript, and SimpleSamlPhP.

Key Definitions

Before we get into the implementation of setting up our SAML-based Single Sign-On (SSO) solution, lets go over some key terms and concepts:

1. SAML (Security Assertion Markup Language): SAML is an XML-based standard used for exchanging authentication and authorization data between parties, typically an identity provider (IdP) and a service provider (SP). It enables secure SSO and facilitates the transfer of user identity information in a standardized format.

2. Identity Provider (IdP): The Identity Provider is a system or service responsible for authenticating users and generating SAML assertions. It vouches for the user’s identity to service providers (SPs) by issuing SAML tokens.

3. Service Provider (SP): The Service Provider is the application or system that relies on the IdP for user authentication. It consumes SAML assertions from the IdP to grant users access.

4. SAML Assertion: A SAML assertion is an XML document that contains information about a user’s identity and attributes. It is digitally signed by the IdP to ensure its authenticity and can be used by the SP for user authentication and authorization.

5. SSO (Single Sign-On): SSO is a user authentication process that allows users to access multiple applications or services with a single set of login credentials. Once authenticated, users are seamlessly granted access to others without the need to re-enter their credentials.

6. SAML Metadata: SAML metadata is an XML document that contains information about the IdP and SP, including public keys, endpoints, and entity IDs. It is used by both parties to configure the SAML integration securely.

Now that we’ve covered the basics, let’s move on to exploring the SAML flow in practice.

Understanding the SAML Flow: Step-by-Step

To appreciate how SAML works and why it’s crucial for Single Sign-On (SSO) solutions, let’s break down the typical flow of SAML authentication and authorization. Here’s a step-by-step overview:

Step 1: User Requests Access

The user initiates access to a Service Provider (SP) by trying to log in or access a protected resource within the SP’s application.

Step 2: SP Redirects to IdP

The SP identifies that the user is not authenticated and redirects them to the Identity Provider (IdP) for authentication. This redirect is typically achieved through an HTTP redirect binding.

Step 3: User Authenticates with IdP

The user lands on the IdP’s login page and provides their credentials, such as username and password.

Step 4: IdP Generates SAML Assertion

Upon successful authentication, the IdP generates a SAML assertion. This assertion includes information about the user’s identity and attributes, signed by the IdP’s private key to ensure its integrity.

Step 5: IdP Sends SAML Response

The IdP sends the SAML assertion as part of a SAML response to the user’s browser. This response is sent via the user’s browser, often using an HTTP POST or Redirect, back to the SP.

Step 6: SP Validates and Processes SAML Assertion

The SP receives the SAML response and validates the signature on the SAML assertion using the IdP’s public key. If the signature is valid, the SP extracts user information and attributes from the assertion.

Step 7: User Authenticated at SP

With the SAML assertion validated and user information extracted, the SP considers the user authenticated. The user is granted access to the requested resource or application.

Step 8: User Accesses Protected Resource

The user is now logged in and can access the protected resource within the SP’s application without needing to re-enter credentials.

Step 9: Continuous User Access

For subsequent accesses to different SPs in the same SAML SSO environment, the user does not need to log in again. The SAML assertion can be reused across SPs, providing a seamless and secure SSO experience.

Understanding this flow is essential for successful SAML-based SSO implementation. But before we dive into the setup, let’s first ensure we have all the prerequisites in place.

Prerequisites

Before setting up your SAML-based Single Sign-On (SSO) environment, ensure you have the following prerequisites in place:

  1. Docker: Docker is a platform that enables developers to create, deploy, and run applications in isolated containers, streamlining software development and deployment processes. We will use Docker to run a local Identity Provider (IdP). You can download and install Docker from Docker’s official website.
  2. Node.js and NVM (Node Version Manager): Node.js is the runtime environment for executing JavaScript, and NVM allows you to manage multiple Node.js versions on your system. You can install NVM and Node.js by following the instructions at Node Version Manager (NVM) GitHub. Note I used Node version 18 for this project (lts/hydrogen).
  3. OpenSSL (For Certificate Management): OpenSSL is a tool used in the context of SAML for generating SSL/TLS certificates and keys. These certificates are used for secure communication and encryption/decryption of SAML assertions and messages. You’ll need OpenSSL to create and manage these certificates as part of your SAML setup.

Now that we have the prerequisites in place, let’s dive into setting up our Identity Provider (IdP) using Docker and SimpleSamlPhP.

Setup our Identity Provider (IdP)

In this section, we will set up and Identity Provider (IdP) using Docker. We’ll be using the kristophjunge/test-saml-idp image, which runs the SimpleSamlPhP Identity Provider.

Docker Compose Configuration

First, let’s create a Docker Compose file named docker-compose.yml with the following content:

version: '3'
services:
testsamlidp_idp:
image: kristophjunge/test-saml-idp
container_name: testsamlidp_idp
ports:
- "8080:8080"
- "8443:8443"
environment:
- SIMPLESAMLPHP_SP_ENTITY_ID=saml-poc
- SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:4300/login/callback
- SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE=http://localhost:4300/logout/callback

Now, let’s break down the content of this Docker Compose file line by line:

  • version: '3': This line specifies the version of the Docker Compose file format being used. In this case, we're using version 3.
  • services:: The service section is where you define the Docker containers you want to run.
  • simple-saml-php:: This is the name of the service/container we're defining. You can choose any name you prefer.
  • image: kristophjunge/test-saml-idp: This line specifies the Docker image to use for this service. In this case, we're using the kristophjunge/test-saml-idp image from Docker Hub, which includes the SimpleSamlPhP IdP.
  • container_name: simple-saml-php: Here, we set a custom name for the Docker container. You can choose a different name if needed.
  • ports:: This section defines the port mappings between the host machine and the container.
  • "8080:8080": This maps port 8080 on the host machine to port 8080 in the container. Port 8080 is where the HTTP IdP web interface will be accessible.
  • "8443:8443": Similarly, port 8443 on the host is mapped to port 8443 in the container. Port 8443 is where the HTTPS IdP web interface is accessible. If you are trying to test locally with HTTPs, you will need use the 8443 port instead of the 8080 port. Note you will also need to run Express with an SSL certificate.
  • SIMPLESAMLPHP_SP_ENTITY_ID=saml-poc: This variable defines the Entity ID for the Service Provider (SP). It's a unique identifier for your application within the IdP.
  • SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:4300/login/callback: This specifies the Assertion Consumer Service (ACS) URL where the IdP should send SAML assertions after user authentication.
  • SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE=http://localhost:4300/logout/callback: This sets the Single Logout Service (SLS) URL for handling SAML single logout requests.

Starting the Identity Provider

With the Docker Compose file in place, you can now start the Identity Provider by running the following command in the directory containing the docker-compose.yml file:

docker-compose up

This command will initiate the IdP container and make it accessible on the specified ports. You can access the IdP’s web interface at http://localhost:8080/simplesaml/module.php/core/frontpage_welcome.php.

With our Identity Provider (IdP) now running, let’s shift our focus to the Service Provider (SP).

Setup our Service Provider (SP)

npx create-nx-workspace

You should now see and installation wizard,

  1. Where would you like to create your workspace? saml-sso-express-passport-docker-example
  2. Which stack do you want to use? node
  3. What framework should be used? express
  4. Integrated monorepo, or standalone project? integrated
  5. Application name: express-api
  6. Would you like to generate a Dockerfile? No
  7. Enable distributed caching to make your CI faster? No

You should now see a folder named: saml-sso-express-passport-docker-example

Navigate into the folder and now lets install our missing dependencies:

npm i cookie-parser body-parser express-session passport @node-saml/passport-saml --save

Lets install some types:

npm i @types/express @types/express-session @types/body-parser @types/cookie-parser --save-dev

Now that we have our workspace ready, lets generate the necessary certificate and key PEM files for secure communication.

Generating Certificate and Key PEM Files for the Service Provider

To ensure secure communication and cryptographic operations between our Service Provider (SP) and Identity Provider (IdP) in the context of SAML-based Single Sign-On (SSO), we’ll generate both a private key (key.pem) and a public key (cert.pem) PEM file using OpenSSL. Additionally, we’ll obtain the IdP’s public key (idp.pem) from its metadata.

Run the following OpenSSL command to generate thekey.pem and cert.pem pem files:

openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out cert.pem

This command will generate the key.pem file, which is crucial for decryption and SAML configuration, and the cert.pem file, representing the public key. The public key (cert.pem) will be used to generate the service provider metadata. The idp.pem cert will be used for signature validation, ensuring the authenticity and integrity of SAML messages coming from the IdP.

We can find the idp.pem cert, by finding it in the SimpSamlPhP metadata:

http://localhost:8080/simplesaml/saml2/idp/metadata.php

Note the X509 certificate element in the XML document contains the key, specific to the IdP. We just need to create the key by creating a new file called idp.pem and paste the following content into it:

-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav
Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+
YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc
+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix
YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8
jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C
YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b
lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs
X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7
yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7
NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n
aQ==
-----END CERTIFICATE-----

Once all the PEM files (key.pem, cert.pem, and idp.pem) have been created and obtained, let's organize them into a folder called certs to keep our certificates and keys well-organized.

Now that our keys and certificates are ready and organized, we set up express and our SAML strategy.

Setup Express and Create Passport SAML Strategy

And finally, lets update our main.ts file to:

import express from 'express';
import cookieParser from 'cookie-parser';
import bodyParser from 'body-parser';
import session from 'express-session';
import passport from 'passport';
import { Strategy, VerifyWithoutRequest } from '@node-saml/passport-saml';
import { readFileSync } from 'fs';

const samlStrategy = new Strategy({
callbackUrl: 'http://localhost:4300/login/callback',
entryPoint: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',
issuer: 'saml-poc',
decryptionPvk: readFileSync(`./certs/key.pem`, 'utf8'),
privateKey: readFileSync(`./certs/key.pem`, 'utf8'),
cert: readFileSync(`./certs/idp.pem`, 'utf8')
},
((profile, done) => done(null, profile)) as VerifyWithoutRequest,
((profile, done) => done(null, profile)) as VerifyWithoutRequest
);

passport.serializeUser(function(user, done) {
console.log(`serialize user`, user);
done(null, user);
});
passport.deserializeUser((user, done) => done(null, user));
passport.use('samlStrategy', samlStrategy);

const app = express();

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(session({ secret: 'secret', resave: false, saveUninitialized: true }));
app.use(passport.initialize({}));

const host = 'localhost';
const port = 4300;
app.listen(4300, 'localhost', () => console.log(`[ ready ] http://${host}:${port}`));

app.get('/login', passport.authenticate('samlStrategy'));

app.post('/login/callback',
passport.authenticate('samlStrategy'),
(req, res) => res.send(`Login Successful`)
);

app.route('/metadata').get(function(req, res) {
res.type('application/xml');
res.status(200);
res.send(
samlStrategy.generateServiceProviderMetadata(
readFileSync('./certs/cert.pem', 'utf8'),
readFileSync('./certs/cert.pem', 'utf8')
)
);
});

Now lets go over the code section by section:

const samlStrategy = new Strategy({
callbackUrl: 'http://localhost:4300/login/callback',
entryPoint: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',
issuer: 'saml-poc',
decryptionPvk: readFileSync(`./certs/key.pem`, 'utf8'),
privateKey: readFileSync(`./certs/key.pem`, 'utf8'),
cert: readFileSync(`./certs/idp.pem`, 'utf8')
},
((profile, done) => done(null, profile)) as VerifyWithoutRequest,
((profile, done) => done(null, profile)) as VerifyWithoutRequest
);

Here we are implementing the SAML strategy.

  1. callbackUrl: Specifies the callback url used by the IdP, it must match the same issuer value specified in the Docker compose file.
  2. entrypoint: Specifies the entrypoint used by the IdP, it must match the same issuer value specified in the Docker compose file.
  3. issuer: Specifies the issuer value used by the IdP, it must match the same issuer value specified in the Docker compose file.
  4. decryptionPvk: Specifies the key used for decryption on the SP side. It’s necessary for decrypting SAML assertions received from the IdP.
  5. private: Specifies the key used in the SP’s cryptographic operations, such as signing SAML requests and assertions.
  6. cert: Specifies the public key or certificate used by the SP to verify digital signatures on SAML responses and assertions received from the IdP.
  7. The first callback is for verifying the login.
  8. The second callback is for verifying the logout.
passport.serializeUser(function(user, done) {
console.log(`serialize user`, user);
done(null, user);
});
passport.deserializeUser((user, done) => done(null, user));
passport.use('samlStrategy', samlStrategy);

This code block configures user serialization and deserialization with Passport.js and associates the SAML authentication strategy (samlStrategy) with Passport.

The passport.serializeUser function defines how user objects are serialized before being stored in a session, typically for authentication purposes. In this case, it logs user information for debugging and then passes the serialized user object for storage.

The passport.deserializeUser function is responsible for deserializing user objects from the session when needed, allowing stored user data to be retrieved. In this instance, it simply passes the user object without modifications.

Lastly, passport.use('samlStrategy', samlStrategy) associates the previously defined SAML authentication strategy (samlStrategy) with Passport, making it available for authenticating users in the application.

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(session({ secret: 'secret', resave: false, saveUninitialized: true }));
app.use(passport.initialize({}));

This code block configures various middleware for our Express application:

  1. app.use(cookieParser()): This middleware initializes cookie parsing, which is useful in handling SAML-based Single Sign-On (SSO) as it allows the application to work with cookies sent by the Identity Provider (IdP) during the SSO process.
  2. app.use(bodyParser.urlencoded({ extended: false })): This middleware is essential when dealing with SAML SSO because it parses URL-encoded data, making it suitable for handling SAML request and response parameters sent as form data during the SSO flow.
  3. app.use(bodyParser.json()): Similarly, this middleware is crucial for SAML SSO as it parses JSON data. While SAML primarily uses XML, JSON may be employed in certain parts of the SAML configuration or customizations.
  4. app.use(session({ secret: 'secret', resave: false, saveUninitialized: true })): Session management middleware plays a critical role in SAML-based SSO. It helps maintain user sessions between the Service Provider (SP) and Identity Provider (IdP), allowing users to remain authenticated across multiple interactions during the SSO process.
  5. app.use(passport.initialize({})): This middleware initializes Passport, It sets up Passport.js for handling the authentication process, including redirection to the IdP for login and processing SAML responses.
app.get('/login', passport.authenticate('samlStrategy'));

app.post('/login/callback',
passport.authenticate('samlStrategy'),
(req, res) => res.send(`Login Successful`)
);

In this code block, we are setting up two essential endpoints for SAML-based Single Sign-On (SSO).

  1. app.get('/login', passport.authenticate('samlStrategy')): This route initiates the SSO process when users access '/login.' It triggers the 'samlStrategy' for authentication, redirecting users to the Identity Provider (IdP) for login.
  2. app.post('/login/callback', passport.authenticate('samlStrategy'), (req, res) => res.send('Login Successful')): The '/login/callback' route handles the SAML response from the IdP after successful authentication. It uses the 'samlStrategy' to process the response. Upon success, it sends a 'Login Successful' message to users.

Now that our Service Provider is configured, we’re ready to run the Express server and proceed with the login process.

Running the Express Server and Initiating SAML Login.

With our Service Provider (SP) configuration in place, it’s time to launch the Express server and begin the Single Sign-On (SSO) process. Follow these steps to test the integration:

  1. Start the Express Server: To start the SP, open your terminal and run the following command within your project directory:
nx serve express-api

2. Access the Login Page: Open your web browser and navigate to the following URL: http://localhost:4300/login

3. Log in with Test Credentials: On the IdP login page, you can use the following test credentials for authentication:

  • Username: user1
  • Password: user1pass

4. Victory Dance: You should now see the message Login Successful and the user data in the terminal running the Express app. Do a victory dance!

As we wrap up our exploration of SAML-based Single Sign-On (SSO) with Express.js, let’s summarize the key takeaways before we conclude this journey.

Thats All Folks

In this guide, we’ve explored the implementation of SAML-based Single Sign-On (SSO) in an Express.js application. We started by understanding the basics of SAML, the roles of Identity Providers (IdPs) and Service Providers (SPs), and the SAML flow. We’ve set up the necessary environment, configured SAML strategies, and witnessed the SSO process in action.

SAML SSO offers a secure and efficient way to implement single sign-on in applications, enhancing both user experience and security. It’s a crucial authentication mechanism used across various industries.

As you continue your journey, feel free to customize and adapt your SAML integration to specific use cases. The possibilities are endless, and SAML-based SSO remains a valuable tool in your authentication toolkit.

Explore further, consult the documentation, and engage with the developer community for support. Happy coding and may your SAML-based SSO implementations be seamless and secure!

--

--