Setup a Single Sign On SAML Test Environment with Docker and NodeJS

Jeffry Houser
Jul 11, 2019 · 17 min read

I’m Jeffry Houser, a developer from the Polaris team in the content engineering group at Disney Streaming Services. Polaris was named after Magneto’s daughter from the X-men, and we builds internal tools that allow editors to create magic for our customers in the video services we power.

When working on one of our tools, we needed to Integrate with a single sign on system that uses SAML for authentication. Setting up a local environment for testing SAML was not a trivial task. A lot of articles we found recommended using Feide OpenIdP as a test provider, however that shut down years ago. Additionally, many of the code samples used outdated libraries, leaving some gaps in our knowledge. It took some trial and error to piece together a working solution and I’m going to teach you how we did it.

Defining Terms

The first time I was exposed to it; SAML was difficult for me to get my head around. As such, I’m going to start out with some definitions that will help you understand the pieces of a SAML application.

  • Single Sign On (SSO): Any system that allows authentication code and login data to be shared across multiple applications.
  • Security Assertion Markup Language (SAML): A framework, and XML schema, for implementing Single Sign On.
  • Principal: The user who is attempting to gain access to our application.
  • Assertions: Data about the principal which are included as part of the SAML response. Samples of this might be the user’s name, or other permission data.
  • Service Provider (SP): This is the application, or system, that the user is attempting to access. We will build a simple SP as part of this article.
  • Identity Provider (IdP): This is a remote application, or system, that authenticates the user and returns data back to the service provider. We’re not going to build an IdP from scratch, but I’ll show you how to set up and use a pre-built one.
  • Globally Unique Identifier: A value that the IdP will use to identify an SP.

Knowing the definitions is a great start but knowing how these pieces work together is even more important and I’ll go over that next.

Review the Application Flow

This is a common flow for a SAML application:

Let’s follow the flow:

  1. The Principal — AKA User — tries to access your Service Provider — AKA your application.
  2. The Service Provider checks to see if it knows the Principal. In a browser-based app, this session information would probably be stored as a cookie, but a desktop or application server may store that information in memory. If the user is known, we can load the app normally, so move onto step 8. If the user is not known, jump to step three to start the authentication process.
  3. If no user is known, the SP creates a SAML Request and sends that request to the IdP. This request will contain the Globally Unique Identifier so that the IdP knows which application the principal requested access to.
  4. Now the IdP handles the request. It will authenticate the user. It may do this based on an existing session from a previous sign in, or it may have the user login anew.
  5. Did the IdP successfully collect user details on the Principal? If so, go to step 7, the success set. Otherwise go to step 6 the failure step.
  6. If the Principal was not able to login, the IdP will handle authentication errors and the SP will know nothing about the failure.
  7. If the IdP successfully logged the user in, it will create a SAML response packet, including assertions about the user, and send the info back to the SP’s callback URL. The SP will use that data to create a user session.
  8. If the Principal authenticated properly, then load up the app and let them in.

The rest of this article will focus on steps 3 through 7.

Install Prerequisites

You’ll want to install a prerequisites before we start jumping into the code:

  • Docker: Docker is a container platform that lets us easily create virtual machines with predefined code. We’re going to use it to easily create our own Identity Provider.
  • NodeJS: We are going to write our Service Provider from scratch using a NodeJS and some common plugins.
  • OpenSSL: OpenSSL can be used to create public and private key certificates for. Certs like these are often used for SSL on web sites, but we’re going to use them to encrypt and decrypt the packets we’re sharing between our SP and IdP.

The install instructions at the respective sites will give better setup instructions than anything I could provide here.

Setup our Identity Provider

Creating an identity provider is hard and complicated, so we’re going to use an application that is easily configurable and will run it in a docker container. We’re going to use the SimpleSamPHP IdP application and run it in an existing docker container.

The container is already loaded into the Docker hub, so we can download that without needing to build it from the source. Run this docker command at your command line:

docker pull kristophjunge/test-saml-idp

You’ll see something like this:

Now you should be able to run the docker image:

docker run — name=testsamlidp -p 8080:8080 -p 8443:8443 
-e SIMPLESAMLPHP_SP_ENTITY_ID= saml-poc
-e SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:4300/login/callback
-d kristophjunge/test-saml-idp

There is a lot going on in this command, and it can be confusing if you are not familiar with Docker. Let’s look at each part of the command:

  • docker run: This tells docker to run a new container
  • — name=testsamlidp: This tells Docker that the name of our local container will be testsamlidp.
  • -p 8080:8080 -p 8443:8443: This is a port mapping. Docker listens to the external port 8080 and maps it to the internal port 8080. Additionally, it listens to port 8443 will map to port 8443 in the docker container. The two ports are for http and https traffic into our IdP.
  • -e SIMPLESAMLPHP_SP_ENTITY_ID=saml-poc: This passes in an argument to our docker container. This defines the Globally Unique Identifier for the service provider. We’ll use this value in our SP code later
  • -e SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE=http://localhost:4300/login/callback: This is another argument we’re passing into the docker container. We’re telling it where to redirect to after a successful login. When we build out the Service Provider, it will be on port 4300 at our localhost.
  • -d: The d argument tells us to run the container in the background, and print out the ID.
  • kristophjunge/test-saml-idp: This tells docker which image to use for our container.

Run the command and you’ll see something like this:

You can run this command

docker ps

to make sure that the docker image is running:

Try to load the SAML IdP provider in your browser by going to this URL:

http://localhost:8080/simplesaml

You should see something like this:

Click the Authenticated Tab:

Then click on Test configured authentication sources which will bring you to

Then click `example-userpass` link. Opening this URL to your localhost should bring you directly there:

There are two default users created in this app by default:

UID | Username | Password  | Group  | Email
==================================================================
1 | user1 | user1pass | group1 | user1@example.com
2 | user2 | user2pass | group2 | user2@example.com

Enter one of these users and click the login button, you should see the user information output to the screen:

Click around and go back to the login screen. You will not be presented with another login screen until you log out — or until your cookie expires. The IdP is keeping track of your session login and this is independent of the SPs session tracking.

If you think about your day and something that uses single sign on, you’ll realize that you don’t sign on all that much. Google is a great example. I probably access a dozen or so apps that integrate with a single sign on provider — Google Calendar, Gmail, and YouTube are some examples, but will often go for days without signing back in. This caching mechanism on the IdP side allows me to log in once a day, while still have access to all other services.

Create a Service Provider

With the IdP all ready to go, it is time to create the service provider to integrate with it.

Setup The Node Libraries

You should already have NodeJS installed if you followed the perquisite sets earlier in the article. Run:

npm init

In a blank directory to create the project. Enter these values:

  • Package Name: aop-sp
  • Version: 1.0.0
  • Description: Art of Possible Service Provider
  • Entry Point: index.js
  • Test command: [leave blank]
  • Git repository: [leave blank]
  • Keywords: leave blank
  • Author: Your Name
  • License: (ISC)
  • Is this Okay? Yes

You should see something like this:

Now let’s start installing some Node packages. First, install Express:

npm install express

You should see something like this:

Express is a web server for NodeJS, and we’ll use that as part of our system.

Now install express-session, which allows us to create a server side session associated with a cookie:

npm install express-session

You should see something like this:

Next we want to install an express body parser

npm install body-parser

You’ll see this:

The body parser will parse the responses from incoming requests and let our code access an object instead of trying to deal with raw data as part of the request.

Next, load up the cookie-parser, which copies the cookie header of incoming requests to an object of cookie names:

npm install cookie-parser

You’ll see this:

Next install the Passport Library:

npm install passport

Passport is a very popular authentication framework used on top of Node. You should see this:

Finally, install passport-saml. This is a SAML plugin to the Passport library.

While the Passport library provides a framework for handling authentication, it is extensible to allow for different approaches to be plugged into it. These approaches are called strategies, and the passport-saml library is a SAML strategy for Passport. We use it so that we do not have to manually create packets when requesting a login from the authentication library or process the packets that get returned. This library makes our lives easier by doing that for us.

If you’ve been following along, you should have a package.json that looks something like this:

{
"name": "aop-sp",
"version": "1.0.0",
"description": "Art of Possible Service Provider",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jeffry Houser",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.4",
"express": "^4.16.4",
"express-session": "^1.16.1",
"passport": "^0.4.0",
"passport-saml": "^1.0.0"
}
}

Create an empty index.js file in the same directory as the package.json, we’ll get to populating that shortly.

Create Certs

We are going to need three certificate files for our application. First, we’ll use openSSL to create a public and private cert for our application. We’ll encrypt our SAML requests with our private key, and the IdP will use the public key to decrypt them. The IdP will encrypt the responses with its private key, and we’ll use its public key to decrypt the responses.

First, let’s use OpenSSL to create our own keys. Run this at your console:

openssl req -x509 -newkey rsa:4096 -keyout certs\key.pem 
-out certs\cert.pem -nodes -days 900

You’ll need to create your certs directory before running that command. This command will step you through a wizard asking you for pertinent information to the key generation. Here are the values I entered:

  • Country Name: US
  • State or Province: Connecticut
  • Locality: Central
  • Organization Name: ArtOfPossible
  • Organizational Unit: BlogWriter
  • Common Name: JeffryHouser
  • EmailAddress: [This space intentionally Left Blank]

You should see something like this with the final results:

You’ll have two files generated from this:

  • cert.pem: Your Public cert
  • key.pem: Your Private cert.

We’ll reference these from our code when creating our service provider. We need one more cert, the IdP’s public cert. To get that, open up the IdP’s metadata page:

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

You should see:

Look for the X509Certificate tag in the XML and copy it to a file named idp_key.pem in your certs directory.

Setup Express Web Sever

Let’s set up Express. Open up the empty index.js. Start with some imports:

var express = require("express");
var session = require('express-session');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');

This imports the express web server and three plugins — express-session, body-parser, and cookie-parser.

Now create an instance of Express, and save to the app variable:

var app = express();

Now, we tell the Express instance to use the other plugs. First, the cookieParser:

app.use(cookieParser());

Cookies will be required to tell whether the user is authenticated or not. This will be done behind the scenes by the passport library.

Now set up the bodyParser:

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

The body parser can turn the body text of a URL request into a simple object for us to access. The urlencoded() command will handle `application/x-www/form-urlencoded` values. The json() command will take care of any JSON values.

Finally, set up the session:

app.use(session({secret: 'secret', 
resave: false,
saveUninitialized: true,}));

The secret value is used to sign a sessionID cookie. The sessionID will reference the server-side session. We can use any value we want for the secret key, but for the purposes of this sample I made it simple. The resave value determines whether to save the session value back into the session store after every request, even if it was not changed. Typically there is no need to do this. The saveUninitailized value is set to true. This means that a session is always saved after it was created even if it did not change.

Now let’s create a handler for the root of our application:

app.get('/',
function(req, res) {
res.send('Test Home Page');
}
);

This is a get() request on the express app variable. It looks for the root directory, ‘/’. The way that express works is that each request is a collection of functions. The function accepts the request and response arguments, abbreviated to req and res respectively. In this case, we only run a single function which returns the text ‘Test Home Page’. Later we’ll run functions to validate users.

Finally, add some code to start the server:

var server = app.listen(4300, function () {
console.log('Listening on port %d', server.address().port)
});

This will listen to port 4300, and show something on the console to prove that is listening.

Run the server:

node index

You should see:

Load the root in your browser at http://localhost:4300 :

Pop open the web developer tools to look at your cookies. Go to the Application tab and expand cookies under storage:

You should see the connect.sid cookie which was created by the express server. This is the session identifier that the server uses to track you between browser requests. Your browser is successfully hooked up to a server session, even though no data is stored in it yet.

Configure Simple-SAML

Now we’re ready to setup the Passport and SAML libraries. First load the libraries:

var passport = require('passport');
var saml = require('passport-saml');
var fs = require('fs');

I’ve already mentioned passport and passport-saml, but I also add the fs library, which will let us access the file system. We’ll use that to load our certs from disk into the SAML configuration.

Passport requires that we add functions to serialize and deserialize the user, so that is the first thing we’ll do:

passport.serializeUser(function(user, done) {
console.log('-----------------------------');
console.log('serialize user');
console.log(user);
console.log('-----------------------------');
done(null, user);
});
passport.deserializeUser(function(user, done) {
console.log('-----------------------------');
console.log('deserialize user');
console.log(user);
console.log('-----------------------------');
done(null, user);
});

These functions are default functions that just output the user to the console, which is a great debugging tool.

We need to set up a samlStrategy, so that Passport knows how to create requests and process the login. Start with this:

var samlStrategy = new saml.Strategy({
// config options here
}, function(profile, done) {
return done(null, profile);
});

The saml.Strategy() accepts two arguments. The first is a configuration object, which I left blank for the moment, and the second is a function which processes the user. The first argument into the function is a profile object, and the second is done, a callback. For our purposes, we are just loading executing the callback and sending it the profile object unchanged. If we needed to do more functionality, such as load application specific permissions from a database, this could be done here.

Now, let’s populate the configuration object with values. I decided to drop these in one by one so I could explain each one:

callbackUrl: 'http://localhost/login/callback',

The callbackUrl is a URL in our application — the service provider — where the IdP will post back to after a successful user authentication. We haven’t created this URL yet, but we will.

entryPoint: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',

The entryPoint is the URL in the IdP that we will send our request to in order to let the user authenticate. When we ran the docker image to create our local IdP, we passed this value into it as a configuration option named SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE.

Now the issuer:

issuer: 'saml-poc',

The issuer is a globally unique identifier for our application. When we ran the docker image, we passed this value into it as a configuration option named SIMPLESAMLPHP_SP_ENTITY_ID.

identifierFormat: null,

The identifierFormat is a specific format you can request from the IdP. We’re leaving it null here, but most likely your IdP administrators will provide a value that you must enter.

Now set up the keys:

decryptionPvk: fs.readFileSync(__dirname + '/certs/key.pem', 'utf8'),
privateCert: fs.readFileSync(__dirname + '/certs/key.pem', 'utf8'),

The decryptionPvK and privateCert both refer to the local private key we generated. They are used to encrypt the authentication request before we send it to the IdP. This is where I’m using the fs library to load the cert from disc.

validateInResponseTo: false,

The validateInResponseTo value will determine if the incoming SAML responses need to be validated or not. I set it to false for simplicity in our sample.

disableRequestedAuthnContext: true

The disableRequestdAuthnContext is another Boolean value. This can be helpful when authenticating against an Active Directory Server.

That completes our samlStrategy configuration object.

After the strategy creation, tell passport to use the samlStrategy:

passport.use('samlStrategy', samlStrategy);

Simple enough, now initialize passport:

app.use(passport.initialize({}));
app.use(passport.session({}));

The session() is middleware that allows for persistent login — AKA keeping track of users.

Create Login Routes

Let’s create the login handler. It is pretty simple:

app.get('/login',
function (req, res, next) {
console.log('-----------------------------');
console.log('/Start login handler');
next();
},
passport.authenticate('samlStrategy'),
);

I’m using a get() handler on the app variable, and the value is ‘/login’. That means when I load `http://localhost:4300/login` it will run the functions, one after each other. The first function just outputs to the console that the log is executed, and then calls next(). The next() function is a reference to the next handler function. For the next handler function, we are just telling the passport library to authenticate using the ‘samlStrategy’. This will redirect to the IdP which will handle login, and post results back to a `login/callback` handler.

Here is the callback handler:

app.post('/login/callback',
function (req, res, next) {
console.log('-----------------------------');
console.log('/Start login callback ');
next();
},
passport.authenticate('samlStrategy'),
function (req, res) {
console.log('-----------------------------');
console.log('login call back dumps');
console.log(req.user);
console.log('-----------------------------');
res.send('Log in Callback Success');
}
);

This calls the post() method on the express instance, app. The URL is the first argument of the method, ‘/login/callback/’. First there is a function, which just logs the currently running request; and calls next() so that the next function can run. The next function is the passport.authentication() call. This is the same code that we had in login, but here in the callback it sees that we have a return value from the IdP and processes it by calling the serializeUser() function we set up earlier. Then it calls the next function, which outputs the user returned from the service.

Try this. First load up:

http://localhost:4300/login

You won’t see anything, but you’ll automatically be redirected to the IdP login screen:

Look at your web server console:

You’ll see that the login handler was properly hit before the redirect.

Now Enter user1 and user1pass and click Login:

You can see that the “Login in CallBack Success” is loaded in the page’s body. Checking the cookies in the web developer tools you see three:

  • PHPSESSIDIDP: This is a session identifier set by the IdP.
  • SimpleSAMLAuthTokenIdp: This is a User identifier set by the IdP.
  • connect.sid: This is the session identification token set by our express-session plugin.

Check out the console:

You see after the initial login handler was run; the login callback was run. The serializeUser() dumped the user information out to the console; and then again the callback URL dumped out the same user info. This app demonstrates that the login succeeds even if we aren’t doing anything with it yet. The information you get back in the user object depends primarily on what the IdP is programmed to send you.

Create our own metadata link

You may remember that the IdP had a metadata link. We used that to get the public key we passed into the cert option of our samlStrategy variable. We can create our own metadata route to provide that information to the IdP we are integrating with:

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

This creates a get request for metadata, and we use the generateServiceProviderMetadata() to generate this pages XML. It outputs the public cert in utf8 format. Reload the app with the metadata in here:

The great thing about this metadata page is that we can use it to share our internal details with the IdP and the IdP can use it to share its internal details with us. Hopefully we can use it to automate part of our systems so when data changes on one side, the other doesn’t have to manually make changes.

Final Thoughts

I know this article makes it sound super easy to set this up, but our team stumbled a bit doing it. My success is because I was able to stand on their shoulders, and I’m happy to share this with you.

For our apps, it is important to secure things up and down the stack and integrating this SSO approach was a big step forward and making that happen.

disney-streaming

- A Disney Technology Blog

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store