NodeJS / Passport

Requirements

The cookbook uses the Passport library with the passport-saml plugin running with the Express web application framework.

Runtime Example

Included in the repository is a dockerized application for hands-on learners. It starts an Identity Provider and one Service Provider per example.

Clone the Repository

git clone https://gitlab.code.rit.edu/systems-public/saml-cookbook.git

Start Containers

The examples requires docker and docker-compose to run. You can download these by following instructions from the official Docker website.

After dependencies have been installed, navigate to the ./saml-cookbook/docker folder in a terminal and run the following command.

# Get a coffee, this is going to take a while
docker-compose build && docker-compose up

You can now view the example at https://saml-cookbook.localtest.me/. If you’re having issues verify that the command completed without errors and that localhost:443 is forwarding traffic to your docker containers.

All the code can be found at ./saml-cookbook/docker/<example>/.


Integration and Pre-Requisites

Install passport, passport-saml, and some compatible session package. This library uses express-session.

Extract IdP Settings

Find the IdP metadata. RIT’s IdP metadata can be found here.

You’ll need the entity id, the signing certificate, and the single sign-on service url. All the examples in this cookbook default to using RIT’s SAML IdP metadata.

Configuration

Note

If you’re confused about any of the terms, refer to the glossary.

Selecting an Entity Id

An entity id must be a URI you own. An example of a good entity id is $serviceUrl + /saml2, or the URL of your metadata.

Do not use bare words like widgets or URIs owned by other entities like google.com/widgets.

Create a Keypair

You will need a public and private keypair to encrypt and sign communication between the IdP and your service. You must generate a separate keypair every time you create a new entity id. Included is a one-line example to show how we could easily create a keypair for our service.

openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
    -keyout "service.key" -out "service.crt" -subj "/CN=widgets.rit.edu"

Create Your Settings

Refer to the official documentation for an expanded list of options. The passport-saml library passes options directly to node-saml.

Save this alongside your application or integrate it with an existing settings file. These settings configure how you send requests to the IdP and how you parse responses.

Modify the settings in the SP Section and IdP Section to get started.

Variable Settings
 1/** SP Section **/
 2// base url of your site
 3const BASE_URL = 'https://widgets.rit.edu/';
 4// your generated entity id
 5const SP_ENTITY_ID = 'https://widgets.rit.edu/saml2';
 6// your generated keypair information
 7// base64 encoded private key (PEM) for the SP
 8const SP_PVK = fs.readFileSync('/abs/path/to/service.key', { encoding: 'utf8' });
 9// base64 encoded certificate (PEM) for the SP
10const SP_CERT = fs.readFileSync('/abs/path/to/service.crt', { encoding: 'utf8' });
11/** End SP Section **/
12
13/** IdP Section **/
14// single-sign-on url for your IdP, defaults to Redirect binding
15const IDP_SSO_URL = 'https://shibboleth.main.ad.rit.edu/idp/profile/SAML2/Redirect/SSO';
16// base64 encoded certificate (PEM)
17const IDP_CERT = fs.readFileSync('/abs/path/to/idp.crt', { encoding: 'utf8' });
18/** End IdP Section **/
Common Settings
 1const defaultSamlStrategy = new SamlStrategy(
 2    {
 3        name: 'saml',
 4        callbackUrl: BASE_URL + 'saml2/acs',
 5        entryPoint: IDP_SSO_URL,
 6        issuer: SP_ENTITY_ID,
 7        idpCert: IDP_CERT,
 8        decryptionPvk: SP_PVK,
 9        decryptionCert: SP_CERT,
10
11        signMetadata: true,
12        wantAssertionsSigned: true,
13        wantAuthnResponseSigned: true,
14        disableRequestedAuthnContext: true
15    },
16    /* acs callback */
17    (profile, done) => {
18        // Called after successful authentication, parse
19        // the attributes in profile.attributes and create
20        // or update a local user. Then return that user.
21        return done(null, profile.attributes)
22    }
23    /* end acs callback */
24)
25
26module.exports = {
27    defaultSamlStrategy,
28    IDP_CERT,
29    SP_PVK,
30    SP_CERT
31};

Integrating SAML

Once your settings are taken care of you can begin integration. To integrate with a production IdP, you will need to exchange metadata and define the attributes you need for your service.

An example list of attributes you may need is:

  • User name (uid)

  • First name (givenName)

  • Last name (sn)

  • email (mail)

Generating Metadata

The IdP will require a copy of the metadata produced at this endpoint to register your service.

1siteRoot.get(
2    "/saml2/metadata",
3    (req, res) => {
4        res.set('Content-Type', 'text/xml');
5        res.send(defaultSamlStrategy.generateServiceProviderMetadata(SP_CERT, SP_CERT))
6    }
7)

Creating the AuthnRequest

The passport.authenticate(STRATEGY_NAME) function handles redirecting the user to the IdP with the correct parameters.

1// Passes the SAML login function handler to passport. 
2// Passport will then redirect the client to the IdP
3siteRoot.get('/login', passport.authenticate('saml'))

Parsing the Response from the IdP

The ACS endpoint extracts and operates on a payload set by the IdP. Passport creates a handler for you to do most of this.

 1// Passes the ACS function to passport. 
 2// Passport will then extract the attributes from the IdP
 3// assertion and store the user in the session.
 4siteRoot.post(
 5    "/saml2/acs",
 6    bodyParser.urlencoded({ extended: false }),
 7    passport.authenticate("saml", {
 8        failureRedirect: SITE_ROOT,
 9        failureFlash: true,
10    }),
11    function (req, res) {
12        res.redirect(SITE_ROOT);
13    },
14)

Extracting Attributes

After successfully parsing the IdP payload, the ACS can then extract attributes. These are returned as an object and may be mapped to an alias such as mail, uid, givenName or may use an oid such as 0.9.2342.19200300.100.1.1.

With Passport, the attributes are extracted in the strategy initializer. You can choose to create a local user here and use that user record on subsequent page loads.

1(profile, done) => {
2    // Called after successful authentication, parse
3    // the attributes in profile.attributes and create
4    // or update a local user. Then return that user.
5    return done(null, profile.attributes)
6}

The user payload is then passed to the passport.serializeUser method. This saves attributes to the user’s session. For the example we save all the SAML attributes. In a more robust environment you could extract a user profile id, serialize that, and then lookup the user from the database.

 1// Necessary to tell passport how to serialize the user
 2// In a production environment we may just serialize the
 3// user.id and then read from the database when deserializing
 4passport.serializeUser(function (user, done) {
 5    done(null, user);
 6});
 7
 8// Same as the above, we could just have an id and need to hydrate
 9// that into a full user object. In this example we just store the
10// full attribute array in the session and retrieve it every time.
11passport.deserializeUser(function (user, done) {
12    done(null, user);
13});