Testing SAML flow in your Node.js application

Abhay Ratnaparkhi
Jul 24, 2017 · 6 min read

Security Assertion Markup Language (SAML) is a XML-based framework for authentication and authorization between two entities: a Service Provider (SP) and an Identity Provider (IdP). Many web applications today need to rely on some third party authentication service provider for authenticating and authorizing their users. Most of the internally hosted web applications in large enterprises are now being hosted on internet facing cloud platform. These applications use SAML to redirect users to their Identity provider for authentication purpose.

We will look into how can we enable SAML in sample Node.js web application. We’ll look in to some internals of how SAML works and then look into how can we write tests for verifying SAML flow. Writing tests needs a good understanding of SAML flow and assertions. This will make us write some forge assertions and write tests when assertion fails.

How SAML Works?

SAML protocol enables “Single Sign On” by redirecting users to Identity Provider for authentication. There are many documents which explains how SAML flow works. The wikipedia article explains in detail about how request flows between user agent, Service Provider (SP) and the Identity Provider (IdP)

SAML Authentication Flow

When unauthenticated user makes a request to a resource to SP, he’ll be redirected to IdP for login. User gets authenticated by IdP and then IdP sends SAML Response with signed assertion to SP. SP then validates the response and gives user authorization to the requested resource.

How to setup SAML in Node.js web application?

Passport is a most popular authentication middleware for Node.js application. It is a modular middleware where various strategies can be configured for the authentication purpose. In this application we are using “passport-saml” authentication strategy. The SAML authentication can be configured easily by setting up configuration parameters. Below are few important parameters in the configuration.

  • entryPoint - This shows the URL on IdP where SP should redirect users for login request
  • path — This indicates where IdP should POST SAML Response after authenticating user

How to write test cases for validating SAML flow?

We’ll see how to create a valid and invalid SAML Response and use it in testing SAML authentication flow.

  • Generate Self Signed Certificates

Service Provider (SP) and Identity Provider(IdP) both needs to verify that message is originated from a legitimate source. Public key cryptography ( X.509 certificate) is used to achieve this. Both SP and IdPs have public keys from other party. Whenever either IdP or SP sends message it is encrypted using it’s private key. While testing SAML flow, we don’t need real Identity provider. We can use self signed certificates to sign SAML Response from IdP.

You can generate Self Signed Certificate by using the service from onelogin online tool. This tool creates self-signed certificates that can be used in this test environment. It generates private key and public X.509 certificate. We need private key to sign SAML response from IdP. The public X.509 certificate will be used by SP to decrypt the SAML assertion response from IdP. Alternatively, you can also use xml-crypto npm module to do these things by writing some code. We won’t be looking into xml-crypto in this article.

  • Generate a valid SAML assertion XML document.

You can generate a valid SAML Response from onelogin online tool. You need to provide XML document to sign, private key and X.509 certificate. You also need to mention mode which indicates if you want to sign message, assertion or both. The XML you provide will contain the SAML Response you receive from IdP. Below is a sample XML you can use.

<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Destination="CALLBACK_URL"  IssueInstant="2017-06-20T02:38:40Z" Version="2.0">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">SAML_ISSUER_URL</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion>
<saml:Subject>
<saml:NameID Format="urn:ibm:names:ITFIM:5.1:accessmanager">Harry.Potter@ibm.com</saml:NameID>
</saml:Subject>
<saml:AttributeStatement>
<saml:Attribute Name="firstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">Harry</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">1A0000897</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="lastName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">Potter</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="emailaddress" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">Harry.Potter@ibm.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="cn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">HARRY A. POTTER</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>

Make sure that you use the same CALLBACK_URL and SAML_ISSUER_URL configured in your passport authentication strategy used in Node.js application. You’ll see the “<ds:Signature>” tag added to the SAML Response after the issuer tag.

<ds:Signature>
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfx6a21bf22-2f70-110f-33df-17b9bb49668c">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>28nhkEANaYjTrBl+jfr8RNHPsyA=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>wOAykPt2oRC2l0Mzpp1dF1IkywiITAK4nDJifgpy/fG+L7ab7GEuz4CutpCsh9OIpYSU0I6iHOBLCLy5NiNIUU2gBy56FRAwdOXm1wuFhMkVrbAN/CUC2YhlMeW507mGYcIQfkUIyaP00U8/2UtdDkYZzOVxcxmfIEP+HA6ndrw=</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIC4DCCAkmgAwIBAgIBADANBgkqhkiG9w0BAQ0FADCBjDELMAkGA1UEBhMCdXMxDjAMBgNVBAgMBVRleGFzMQwwCgYDVQQKDANJQk0xETAPBgNVBAMMCGFicmF0bmFwMQ8wDQYDVQQHDAZBdXN0aW4xDzANBgNVBAsMBkF1c3RpbjEqMCgGCSqGSIb3DQEJARYbYWJoYXkucmF0bmFwYXJraGlAZ21haWwuY29tMB4XDTE3MDcyNDE1MjE1NFoXDTE4MDcyNDE1MjE1NFowgYwxCzAJBgNVBAYTAnVzMQ4wDAYDVQQIDAVUZXhhczEMMAoGA1UECgwDSUJNMREwDwYDVQQDDAhhYnJhdG5hcDEPMA0GA1UEBwwGQXVzdGluMQ8wDQYDVQQLDAZBdXN0aW4xKjAoBgkqhkiG9w0BCQEWG2FiaGF5LnJhdG5hcGFya2hpQGdtYWlsLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0/C/bHgdOPdXFTEPk5vDxD8/62o2IPeEVmroklBTTE8yOKGOlBXhG1BBoXHXWUZQK6nYVqmrqwpIIt+9Lfg5I44m/Drvdl2Ke8wIt3pws5cZSvRN9DNDhDyvTovnccBGL0ajnilfBDXc6gB+nZG2EjR9MjLJkrsRk9zG5t0MyIUCAwEAAaNQME4wHQYDVR0OBBYEFOLAEZww3RFZar4Ok6/26ohkIYdTMB8GA1UdIwQYMBaAFOLAEZww3RFZar4Ok6/26ohkIYdTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQENBQADgYEAlzl4tQJqJQ6ucJTT5Yw8Hb6IyPkJ2/82Ei9ZVkAEkh+MdpE/4e6kxBXP+1csaY7IMETqmhPmbs88QLp/b+SIC1f3BmOKh5pqn6CfmAH9b1Iybw78CCZY3ITo1/6waSX8pElYiPZB+T/tYu4WXjWMIidUABw8trjqwRrGK6LV0G4=
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>

This signature has message digest value. It helps in identifying if the response has been tampered in transit. If the response is changed by intruder then this digest value won’t match so authentication will not succeed. It also has a signature value which can only be generated if you have a private key of IdP. The signature can be verified by any party having public key (X.509 certificate). The public key is also embedded in the response.

  • Generate an invalid SAML assertion Response document.

There can be multiple reasons for an invalid SAML Response. Some reasons like unable to parse the response, SAML assertion is expired, the message signature isn’t matching or the message digest isn’t correct etc. You can generate invalid SAML assertion response by changing any one if these values.

You can have “<saml:Conditions>tag in your SAML Response which indicates the validity period of your response. “<saml:AuthnStatement>” tag also has an attribute (SessionNotOnOrAfter) to indicate session validity period. By default IDP sets session validity for shorter duration (~10 mins). If you are using these in valid SAML Response then set it to some maximum value. You can have invalid SAML Response by setting this validity period before current timestamp.

<saml:Conditions NotBefore="2017-06-20T02:37:39Z" NotOnOrAfter="2027-06-20T02:48:40Z">
<saml:AudienceRestriction>
<saml:Audience>passport-app-name</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2017-06-20T02:38:40Z" SessionIndex="uuidc2e4a572-015c-1e66-8ba2-a3354cb7f8a0" SessionNotOnOrAfter="2027-06-20T15:38:40Z">

You can also change digest value or Signature value to make response invalid.

  • Properly encode the SAML document and create a valid and invalid SAML response.

You need to read SAML Response using “utf-8” charset and then do “base-64” encoding followed by “url encoding” any wrong encoding will result in an invalid SAML Response.

const validSAMLAssertion = fs.readFileSync(
path.join(process.cwd(), 'signed-assertion.xml'), 'utf8');
const encodedValidSAMLResponse = urlencode(base64.encode(validSAMLAssertion));const ivalidSAMLAssertion = fs.readFileSync(
path.join(process.cwd(), 'signed-assertion-invalid-signature.xml'), 'utf8');
const encodedInvalidSAMLResponse = urlencode(base64.encode(ivalidSAMLAssertion));
  • Write test cases to test valid and invalid SAML flow

The test cases shown below use Ava testing framework and Supertest module to test HTTP requests.

Below test case shows a successful case of SAML authentication flow.

test('Should support login if SAML is enabled', t => {
return t.context.user.get('/bad')
.expect('location', '/login')
.expect(302)
.expect('set-cookie', /test-session=*/)
.then(validateStoredSessionInfo)
.then(() => {
// check location is IDP location also possibly if SAMLResponse param is correct
return t.context.user.get('/login')
.expect(302)
.expect('location', /test-idp-server*/);
})
.then(() => {
// Assume successful login and Redirect from IDP
return t.context.user.post(config.passport.saml.path)
.send(`SAMLResponse=${encodedValidSAMLResponse}`)
.expect('location', '/bad')
.expect(302);
})
.then(validateStoredUserSessionInfo);
});

If user asks for any page he’ll be redirected to the login page (you can check if proper cookie is stored in session. The login url should be redirected to configured SAML entryPoint URL. With a successful login you can do a POST request with encoded valid SAML Response (this indicates IdP has sent proper SAML Response). After successful response the user should get redirected to original page. ( You need to handle redirection logic in your code to test this flow)

Below test case shows a unsuccessful case of SAML authentication flow. The tampered or expired SAML assertion should cause authentication failure.

// Multiple reasons for invalid SAML assertion 
// eg. unable to parse it, saml assertion expired, etc
test('Should error on invalid SAML assertion', t => {
return t.context.user.get('/')
.expect('location', '/login')
.expect(302)
.expect('set-cookie', /test-session=*/)
.then(() => {
// Check location is an IdP location
return t.context.user.get('/login')
.expect(302)
.expect('location', /test-idp-server*/);
})
.then(() => {
// Assume successful login
// Redirect from IdP and send invalid SAML Response
return t.context.user.post(config.passport.saml.path)
.send(`SAMLResponse=${encodedInvalidSAMLResponse}`)
.expect(500);
});
});

The invalid SAML Response is sent which causes authentication failure from “passport-saml” module.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade