May 26, 2021

A primer on SSO with SAML

Implementing features like Single Sign-On (SSO) usually is one of the loathed tasks when building a SaaS product. Bigger customers, especially in the enterprise segment, will require integration with their identity providers like Azure AD or Okta, allowing them to sign in with their existing company account.

For companies, using SSO has some great benefits: Companies can strictly control which employee may access which applications, they can provision or de-provision accounts quickly, and employees only need to remember one set of credentials for all applications used in their company. For your application, it means, that you don't need to store any passwords of users, which is a great relief.

I think one of the biggest difficulties for teams setting up SSO is that it's usually a task on the side, not given enough priority or time, and depending on the customer, involves a lot of trial and error and back-and-forth communication with the customer to get everything set up.

In this post, I'll walk through the concepts of the widely-used Security Assertion Markup Language (SAML) standard, and how to think about SSO for a multi-tenant SaaS product.

Let's first walk through an example flow with SSO enabled.

Sign in with SSO

In most cases, applications will add an option to enter either the company email address or domain configured for SSO. This allows the system to look up which identity provider to use, as with any multi-tenant SaaS application, you'll have more than one customer running on a shared infrastructure.

After finding the identity provider (IdP), your application redirects the user to their company's portal where they are asked to sign in. Once authenticated, they are sent back to your application and ready to go.

In the background, we need some additional pieces of information to decide if the user is who they pretend to be and if we can authenticate them safely.

SAML

For this, we'll use SAML to establish a standardized connection between your application (the service provider or SP) and your customer's identity provider (IdP). When signing in, your application will create an AuthnRequest containing identifying application on the application requesting an identity check (or assertion) and optionally the destination to send the assertion response to.

For increased security, the AuthnRequest can be signed with a shared secret so the identity provider can be sure it was your application, who requested the details. This is important to prevent leaking details about the employee, such as their email, role in the organization, and other personally identifiable information (PII).

For the initial configuration on the IdP-side, you will specify a list of URLs that an assertion may be returned for, so forging an AuthnRequest should not lead to sending data to an untrusted destination.

Once signed in, the user will be redirected either to the primary assertion consumer service (ACS) URL or to the specified one if the AuthnRequest supplied it. As stated before, the latter has to match the set of allowed ACS URLs on the IdP.

<samlp:AuthnRequest
xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
ID="id6c1c178c166d486687be4aaf5e482730"
Version="2.0" IssueInstant="2013-03-18T03:28:54.1839884Z"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
	https://www.contoso.com
</Issuer>
</samlp:AuthnRequest>

The user's browser will then be redirected, sending a POST request to your backend (identified by the ACS URL) with the SAML response which includes the Assertion, a signed object that shares important data on the user (such as their identifier or email) with the service.

Signing the assertion is crucial for your application to be sure that the actual identity provider issued the assertion, and it was not forged. The assertion is usually signed with a secret you received when configuring SSO.

<samlp:Response ID="_a4958bfd-e107-4e67-b06d-0d85ade2e76a" Version="2.0"
IssueInstant="2013-03-18T07:38:15.144Z"
Destination="https://contoso.com/identity/inboundsso.aspx"
InResponseTo="id758d0ef385634593a77bdf7e632984b6"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
	https://login.microsoftonline.com/82869000-6ad1-48f0-8171-272ed18796e9/
</Issuer>
  <ds:Signature xmlns:ds="https://www.w3.org/2000/09/xmldsig#">
    ...
  </ds:Signature>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
  </samlp:Status>
  <Assertion ID="_bf9c623d-cc20-407a-9a59-c2d0aee84d12" IssueInstant="2013-03-18T07:38:15.144Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
    <Issuer>
https://login.microsoftonline.com/82869000-6ad1-48f0-8171-272ed18796e9/
</Issuer>
    <ds:Signature xmlns:ds="https://www.w3.org/2000/09/xmldsig#">
      ...
    </ds:Signature>
    <Subject>
      <NameID>Uz2Pqz1X7pxe4XLWxV9KJQ+n59d573SepSAkuYKSde8=</NameID>
      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData InResponseTo="id758d0ef385634593a77bdf7e632984b6" NotOnOrAfter="2013-03-18T07:43:15.144Z" Recipient="https://contoso.com/identity/inboundsso.aspx" />
      </SubjectConfirmation>
    </Subject>
    <Conditions
NotBefore="2013-03-18T07:38:15.128Z"
NotOnOrAfter="2013-03-18T08:48:15.128Z">
      <AudienceRestriction>
        <Audience>https://www.contoso.com</Audience>
      </AudienceRestriction>
    </Conditions>
    <AttributeStatement>
      <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
        <AttributeValue>testuser@contoso.com</AttributeValue>
      </Attribute>
      <Attribute Name="http://schemas.microsoft.com/identity/claims/objectidentifier">
        <AttributeValue>3F2504E0-4F89-11D3-9A0C-0305E82C3301</AttributeValue>
      </Attribute>
      ...
    </AttributeStatement>
    <AuthnStatement AuthnInstant="2013-03-18T07:33:56.000Z" SessionIndex="_bf9c623d-cc20-407a-9a59-c2d0aee84d12">
      <AuthnContext>
        <AuthnContextClassRef> urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
      </AuthnContext>
    </AuthnStatement>
  </Assertion>
</samlp:Response>

User Attributes and Claims

Depending on your application's needs, you can send a lot of data from the identity provider back to your application through the SAML assertion.

The most important piece of data is the NameIdentifier (or NameID) value, which uniquely identifies the user signing in. If your application uses unique email addresses, you should make sure that your customers' identity providers will return an email address.

Provisioning Users

While SSO allows your users to sign in to your app with their existing company account, they will still need some form of account in your system. In addition to manual registration, you could add support for the System for Cross-domain Identity Management (SCIM), a standard for automating provisioning and de-provisioning of users. This integrates with your customer's identity provider which will let your application know of any users that should be created or removed.

If you're interested, I'll follow up with a guide on the concepts and how to build a SCIM integration next.

Additional Resources


I wanted this post to focus explicitly on giving you an introduction to how SAML works, not how to integrate it as different companies use different tools for this job. Usually you'll pick something like Auth0, but you can build it yourself too. If you would like a post on the latter, send a mail or reach out on Twitter