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.
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 **/
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});