Back to Blog

A gentle introduction to SAML

The SAML spec is an absolute beast. We've each read it multiple times. Here's a simpler explanation.
profile picture
Ned O'Leary
X GitHub
Cofounder and CEO, SSOReady

I recently attended an open-source event in San Francisco. Introducing myself to random strangers – as one does at such events – I soon met a true hacker, a dyed-in-the-wool computer fanatic, the kind of person who’d have worked as a software engineer before it was cool. We chatted for a bit about random projects.

Upon discovering that my company works on SAML single sign-on, this person’s eyes lit up. Oh, oh no, you’re in SAML hell, they exclaimed, obviously recalling some technical side quest gone awry. I’m so sorry, they continued, laughing. I don’t ever want to touch SAML again.

This is a common experience for me. Most developers know basically nothing about SAML at all. On rare occasions, I’ll meet someone like my acquaintance from the meetup, someone who knows a little bit about SAML … and actively avoids it.

And honestly, I think that’s reasonable. SAML occupies an arcane little corner of the B2B internet, and there’s not a lot of coherent information out there. Guides seem either to bury the reader in obscure technical details or vomit AI-generated SEO spam with virtually no useful detail.

I’ve written this post for curious developers who want a foundational understanding of SAML without wading too deeply into the details.

I don’t intend this post as a comprehensive explanation. I’ll elide some important facts and even get things technically incorrect in the hopes of illustrating ideas. I’d analogize the approach to introductory mechanics courses: imagine the object as a sphere and ignore air resistance.

Moreover, I welcome critique and correction here. Reach out at ned.oleary@ssoready.com with any comments. (Thank you to those of you who helpfully flagged some typos for me! Markdown doesn’t have spellcheck.)

SAML as a standard

SAML (or Security Assertion Markup Language), defines a flexible set of rules for exchanging security-related messages in XML.

We typically use SAML to exchange messages in settings that involve three or more independent-ish entities. A very common scenario involves two software systems – made by two different companies – and a user. The two software systems need to exchange information about the user. One system might query the other: what’s this user’s email address? The other might respond: this user’s email address is edwardkoleary@gmail.com.

The two software companies could collaborate on a bespoke integration. But doing so can get really unwieldy. Imagine what happens if you need to build and maintain a lot of bespoke integrations that follow their own rules; it’s kind of a nightmare.

SAML erects scaffolding around the kinds of messages that systems can pass to one another. It exists to help us all follow the same rules, speak the same language. By following standardized SAML rules, we don’t have to invent new, convoluted rules.

So even though SAML can feel really complicated, remember that things could be worse!

What does a SAML message look like?

Here’s an example of a SAML message. Let’s not worry about the details.

All SAML basically looks like this. We’re just moving XML messages around.

SAML defines a syntax for our message and tells us how we can safely process other SAML messages’ contents.

If we receive a <Response>, we know a few things about how it’ll look. We know to look for an <Assertion> within the <Response>. We know how to interpret the <AuthnContext> data within an <Assertion>. We can anticipate what the digital signature will look like. We have guidelines on how to process the message in order to avoid security lapses.

<Response IssueInstant="2003-04-17T00:46:02Z" Version="2.0" ID="_c7055387-af61-4fce-8b98-e2927324b306" xmlns="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
    <saml:Issuer>https://www.opensaml.org/IDP"</saml:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <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="#_c7055387-af61-4fce-8b98-e2927324b306">
                <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#">
                        <InclusiveNamespaces PrefixList="#default saml ds xs xsi" xmlns="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </ds:Transform>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                <ds:DigestValue>TCDVSuG6grhyHbzhQFWFzGrxIPE=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>
            x/GyPbzmFEe85pGD3c1aXG4Vspb9V9jGCjwcRCKrtwPS6vdVNCcY5rHaFPYWkf+5
            EIYcPzx+pX1h43SmwviCqXRjRtMANWbHLhWAptaK1ywS7gFgsD01qjyen3CP+m3D
            w6vKhaqledl0BYyrIzb4KkHO4ahNyBVXbJwqv5pUaE4=
        </ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>
                    MIICyjCCAjOgAwIBAgICAnUwDQYJKoZIhvcNAQEEBQAwgakxCzAJBgNVBAYTAlVT
                    MRIwEAYDVQQIEwlXaXNjb25zaW4xEDAOBgNVBAcTB01hZGlzb24xIDAeBgNVBAoT
                    F1VuaXZlcnNpdHkgb2YgV2lzY29uc2luMSswKQYDVQQLEyJEaXZpc2lvbiBvZiBJ
                    bmZvcm1hdGlvbiBUZWNobm9sb2d5MSUwIwYDVQQDExxIRVBLSSBTZXJ2ZXIgQ0Eg
                    LS0gMjAwMjA3MDFBMB4XDTAyMDcyNjA3Mjc1MVoXDTA2MDkwNDA3Mjc1MVowgYsx
                    CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNaWNoaWdhbjESMBAGA1UEBxMJQW5uIEFy
                    Ym9yMQ4wDAYDVQQKEwVVQ0FJRDEcMBoGA1UEAxMTc2hpYjEuaW50ZXJuZXQyLmVk
                    dTEnMCUGCSqGSIb3DQEJARYYcm9vdEBzaGliMS5pbnRlcm5ldDIuZWR1MIGfMA0G
                    CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZSAb2sxvhAXnXVIVTx8vuRay+x50z7GJj
                    IHRYQgIv6IqaGG04eTcyVMhoekE0b45QgvBIaOAPSZBl13R6+KYiE7x4XAWIrCP+
                    c2MZVeXeTgV3Yz+USLg2Y1on+Jh4HxwkPFmZBctyXiUr6DxF8rvoP9W7O27rhRjE
                    pmqOIfGTWQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIFoDANBgkq
                    hkiG9w0BAQQFAAOBgQBfDqEW+OI3jqBQHIBzhujN/PizdN7s/z4D5d3pptWDJf2n
                    qgi7lFV6MDkhmTvTqBtjmNk3No7v/dnP6Hr7wHxvCCRwubnmIfZ6QZAv2FU78pLX
                    8I3bsbmRAUg4UP9hH6ABVq4KQKMknxu1xQxLhpR1ylGPdiowMNTrEG8cCx3w/w==
                </ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
    <Status>
        <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </Status>
    <Assertion ID="_a75adf55-01d7-40cc-929f-dbd8372ebdfc" IssueInstant="2003-04-17T00:46:02Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
        <Issuer>https://www.opensaml.org/IDP</Issuer>
        <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <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="#_a75adf55-01d7-40cc-929f-dbd8372ebdfc">
                    <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#">
                            <InclusiveNamespaces PrefixList="#default saml ds xs xsi" xmlns="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                        </ds:Transform>
                    </ds:Transforms>
                    <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <ds:DigestValue>Kclet6XcaOgOWXM4gty6/UNdviI=</ds:DigestValue>
                </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>
                hq4zk+ZknjggCQgZm7ea8fI79gJEsRy3E8LHDpYXWQIgZpkJN9CMLG8ENR4Nrw+n
                7iyzixBvKXX8P53BTCT4VghPBWhFYSt9tHWu/AtJfOTh6qaAsNdeCyG86jmtp3TD
                MwuL/cBUj2OtBZOQMFn7jQ9YB7klIz3RqVL+wNmeWI4=
            </ds:SignatureValue>
            <ds:KeyInfo>
                <ds:X509Data>
                    <ds:X509Certificate>
                        MIICyjCCAjOgAwIBAgICAnUwDQYJKoZIhvcNAQEEBQAwgakxCzAJBgNVBAYTAlVT
                        MRIwEAYDVQQIEwlXaXNjb25zaW4xEDAOBgNVBAcTB01hZGlzb24xIDAeBgNVBAoT
                        F1VuaXZlcnNpdHkgb2YgV2lzY29uc2luMSswKQYDVQQLEyJEaXZpc2lvbiBvZiBJ
                        bmZvcm1hdGlvbiBUZWNobm9sb2d5MSUwIwYDVQQDExxIRVBLSSBTZXJ2ZXIgQ0Eg
                        LS0gMjAwMjA3MDFBMB4XDTAyMDcyNjA3Mjc1MVoXDTA2MDkwNDA3Mjc1MVowgYsx
                        CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNaWNoaWdhbjESMBAGA1UEBxMJQW5uIEFy
                        Ym9yMQ4wDAYDVQQKEwVVQ0FJRDEcMBoGA1UEAxMTc2hpYjEuaW50ZXJuZXQyLmVk
                        dTEnMCUGCSqGSIb3DQEJARYYcm9vdEBzaGliMS5pbnRlcm5ldDIuZWR1MIGfMA0G
                        CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZSAb2sxvhAXnXVIVTx8vuRay+x50z7GJj
                        IHRYQgIv6IqaGG04eTcyVMhoekE0b45QgvBIaOAPSZBl13R6+KYiE7x4XAWIrCP+
                        c2MZVeXeTgV3Yz+USLg2Y1on+Jh4HxwkPFmZBctyXiUr6DxF8rvoP9W7O27rhRjE
                        pmqOIfGTWQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIFoDANBgkq
                        hkiG9w0BAQQFAAOBgQBfDqEW+OI3jqBQHIBzhujN/PizdN7s/z4D5d3pptWDJf2n
                        qgi7lFV6MDkhmTvTqBtjmNk3No7v/dnP6Hr7wHxvCCRwubnmIfZ6QZAv2FU78pLX
                        8I3bsbmRAUg4UP9hH6ABVq4KQKMknxu1xQxLhpR1ylGPdiowMNTrEG8cCx3w/w==
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </ds:Signature>
        <Subject>
            <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
                scott@example.org
            </NameID>
            <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/>
        </Subject>
        <Conditions NotBefore="2003-04-17T00:46:02Z" NotOnOrAfter="2003-04-17T00:51:02Z">
            <AudienceRestriction>
                <Audience>http://www.opensaml.org/SP</Audience>
            </AudienceRestriction>
        </Conditions>
        <AuthnStatement AuthnInstant="2003-04-17T00:46:00Z">
            <AuthnContext>
                <AuthnContextClassRef>
                    urn:oasis:names:tc:SAML:2.0:ac:classes:Password
                </AuthnContextClassRef>
            </AuthnContext>
        </AuthnStatement>
    </Assertion>
</Response>

Kind of gnarly.

Flexibility in SAML

SAML’s design emphasizes flexibility; at least in principle, you can do an awful lot of different stuff with SAML. Unfortunately, that flexibility results in complexity.

If you read the SAML specification front to back, you will stumble across countless asides. Here’s one that’s not even that bad (don’t worry about what it actually means):

If it chooses to proxy to a SAML identity provider, when creating the new <AuthnRequest>, the proxying identity provider MUST include equivalent or stricter forms of all the information included in the original request (such as authentication context policy). Note, however, that the proxying provider is free to specify whatever <NameIDPolicy> it wishes to maximize the chances of a successful response.

If the authenticating identity provider is not a SAML identity provider, then the proxying provider MUST have some other way to ensure that the elements governing user agent interaction (<IsPassive>, for example) will be honored by the authenticating provider.

There’s a lot of ifs and for examples here – every one of which adds at least one edge case. We’re talking about hundreds of pages of this stuff.

Now, SAML’s been around for a while, so some people capitalize on its flexibility. In the wild, especially in legacy systems, you will occasionally encounter SAML doing unusual things.

Nearly all of us can make our lives easier. We’ll ignore most of SAML and focus only on the tiny subset that people typically use.

What we really use SAML for

We most commonly use SAML for single sign-on (hereafter abbreviated as SSO). For what it’s worth, SAML actually defines a few weird flavors of SSO, but we generally use the simplest of these, the Web Browser SSO Profile (see 4.1 here for details). We won’t concern ourselves with the more complicated stuff.

Under the Web Browser SSO Profile, end users will access any number of software applications by authenticating first into one centralized system, which then communicates users’ authentication status to the desired software applications. Users don’t authenticate directly with the applications themselves.

If you have ever used a service like Okta to access your email, you’ve likely used the Web Browser SSO Profile.

Entities involved in SSO

For simplicity, let’s imagine you make application software, and your users want to sign in via SAML SSO. You have to figure out how to support SAML SSO in your software.

We care about three entities in SSO:

  1. A user: someone that wants to use your application
  2. A service provider (SP): your application
  3. An identity provider (IDP): the centralized service your user will use to authenticate

We can imagine each customer’s IDP as a database. It keeps track of data about people and their credentials, e.g. hashed passwords, email addresses, names, departments, etc. Companies often use identity providers to assign employees to different departments and assign them different privileges; for instance, the Accounting department will have access to the general ledger, but they won’t have access to AWS.

Every time we want a user to sign in via SAML, we’ll have to get information from their IDP; in SSO, we’re mostly just asking the IDP to confirm the user’s identity.

In order to do so, we will always need a pre-configured trust relationship with the IDP. Every customer that uses SAML SSO will require its own set-up in your application. It doesn’t matter which IDP your customers use – if you have 10 Okta customers, you will need to set up 10 different trust relationships.

Although you will need to establish trust relationships with the IDP, you won’t actually pass messages to/from the IDP directly. In SAML SSO, the service provider and the identity provider communicate via the user’s browser.

How SAML entities interact

Here’s a typical SAML SSO process:

  1. A user attempts to access part of your application in their web browser.
  2. You check whether the user has a valid security context.
  3. The user doesn’t have a valid security context, so you present a login page.
  4. The user fills in some information (e.g. an email address), which you use to determine the appropriate login method. You have pre-set records that tell you (1) that the user requires SAML SSO for authentication and (2) which IDP the user needs to use.
  5. You redirect the user to the IDP’s web address, passing a SAML message to the IDP via the user’s browser.
  6. The IDP prompts your user for credentials. The user successfully authenticates.
  7. The IDP redirects the user back to your application along with a SAML message communicating information about the user’s authentication.
  8. You process the SAML message and determine that you should, in fact, establish a security context for the user.
  9. You grant the user access to the desired part of your application.

A successful login process basically looks like this:

Note that it’s also possible for the user to initiate a SAML SSO process from the identity provider. When this happens, mostly everything works the same way, except the service provider receives an authentication response without having sent an authentication request. If you are implementing SAML SSO support, make sure you’re prepared for IDP-initiated SSO.

Messages exchanged in SAML

You may have noticed that we basically have two messages of interest in SAML SSO: a message from the service provider to the IDP; and then the corresponding message back from the IDP to the service provider.

We call this first message – from the service provider to the IDP – a SAML request. We call the other message – from the IDP to the service provider – a SAML response.

SAML requests aren’t too complicated in practice. We can do a bunch of stuff with them, but it’s often fine simply to send the following XML via HTTP redirect:

<AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="saml_flow_1bkwa10x7lhmh1zvrnhnj76r3" Version="2.0" IssueInstant="2024-05-28T19:18:54.805967185Z">
    <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
        https://auth.ssoready.com/v1/saml/saml_conn_8ufe2hyst7s2jdovzdvd53ldm
    </Issuer>
</AuthnRequest>

We’re just sending a message to an IDP URL that we already know. We include an ID in the <AuthnRequest> tag; this allows the IDP to share a <Response> that we can link back to our original request. We also send the IDP some data wrapped up in an <Issuer> tag. The <Issuer> data tells the IDP who we are. When our user authenticates, the IDP will return the user back to us with a <Response>. Remember, we’re passing this data via the user’s browser, so the IDP won’t know what application the user wants access to unless we specify.

SAML responses are trickier. They usually look something like this, sent via a POST:

<samlp:Response ID="id-9c961a70-708d-497d-8d51-1f8110600838" Version="2.0" IssueInstant="2024-05-28T19:23:17.443Z" InResponseTo="saml_flow_1bkwa10x7lhmh1zvrnhnj76r3" Destination="https://auth.ssoready.com/v1/saml/saml_conn_8ufe2hyst7s2jdovzdvd53ldm/acs" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
        https://auth.pingone.com/c761aef4-f636-402f-b29d-6cc84c13a436
    </saml:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
            <ds:Reference URI="#id-9c961a70-708d-497d-8d51-1f8110600838">
                <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/2001/04/xmlenc#sha256"/>
                <ds:DigestValue>
                    XwEljHxQiNdJYGDSvtNHQsFfQfN7+jAZ8ZJ1pwKqVuE=
                </ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>
            cJbmGIdlBnr1B1//P7mb5ECFK6QZjuH3eqyCinFrSsFMMORNjiT8TJRX8bsAT8lv76zPuBvMOBJSer9Ca7MDLLOQX+u43KufHVLvLarQUWo6fBJIjWsA3UHKFxCaw5h3wUnXZJ94NTqHl0d5An5RgwRcvVLOe3Bdv/hAykNrvQnMRNfbCcD6TWN7IKbl6MDk9o56vR0ubYQ1wqIN3UDcoOUruul9wQ5jtjBo//klKHDr7X+SYqY4oOExVrKNIUhj0DPh89pXKwY3IKR/3yXYas/J8aOWl/f+1bByUcL0jDx4bBc9T7OLw+w/DLMHFwlbxzO4R74WvxryfcVH6GZp7A==
        </ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>
                    MIIDrjCCApagAwIBAgIGAY+TeKX7MA0GCSqGSIb3DQEBCwUAMIGXMQswCQYDVQQGEwJVUzEWMBQGA1UECgwNUGluZyBJZGVudGl0eTEWMBQGA1UECwwNUGluZyBJZGVudGl0eTFYMFYGA1UEAwxPUGluZ09uZSBTU08gQ2VydGlmaWNhdGUgZm9yIFdvcmtmb3JjZSBTb2x1dGlvbiBFbnZpcm9ubWVudCA4MjBiOGQ2OSBlbnZpcm9ubWVudDAeFw0yNDA1MjAwMDQ4MjhaFw0yNTA1MjAwMDQ4MjhaMIGXMQswCQYDVQQGEwJVUzEWMBQGA1UECgwNUGluZyBJZGVudGl0eTEWMBQGA1UECwwNUGluZyBJZGVudGl0eTFYMFYGA1UEAwxPUGluZ09uZSBTU08gQ2VydGlmaWNhdGUgZm9yIFdvcmtmb3JjZSBTb2x1dGlvbiBFbnZpcm9ubWVudCA4MjBiOGQ2OSBlbnZpcm9ubWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI5Xnz5FeA+5zOMYpjLxkksCT5216B9Iq8Y4ly7dQQHMAlTkAz6ONi6itrTTe+4lVKOOexGFikFnsD2zDXEXcBzP/SVxPp4H9bObL0IBAvMZ6WhluK3d6oZDWzw/7SEmcA+T93d/ovqkxrd5+R4+yk0lmKGJOSi+mP0LYqWtjTQqmiMhdZ2mV6Y/8iNg419+5QFNAtrwIcAQA8rI4AUfyiLVZnP1qdxuvY+rkhS54OALAVv14sU2dRiDR5uEmRbjfvVZ92jOEYwmpdLUHz+tTwPWtnhB4+GkQ8bX0Nz+Hzh0GVnk+NMH5+m/A/hl+qG6ea60KaRsIzB4eMxH6GeTTWkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgYYlef5BTuMYkH3MKRV5huyM36WIvak1WF05mxXLq66eXjRjwFklNb2D6Cw6Hn3U/0kHtur3tfdK4o+K6+PHkYkFPS5kYuFDSIcR+WhLxq9ZkdIRg1EM5e8Z+FOAm1ogaSsalu++WLdgw0GJRW0jNaBLn40cABTnNXSjJfCFBgoLZjdHNhwW3A3BrXqg3LCaU1vJSamQceK6okP8dMLU4TFUhcwKlYdd8Wubp/wdnP065LZvmLgpLFPKVpQKsNJXigP71oRtteM9/E5DiPvgScZCQSJ+xVKqAAG2MffmYE+UOFfNlLPW4lG/cxCYVkh8M1iYetad04tHBcL2PSQj7g==
                </ds:X509Certificate>
            </ds:X509Data>
            <ds:KeyValue>
                <ds:RSAKeyValue>
                    <ds:Modulus>
                        jlefPkV4D7nM4ximMvGSSwJPnbXoH0irxjiXLt1BAcwCVOQDPo42LqK2tNN77iVUo457EYWKQWewPbMNcRdwHM/9JXE+ngf1s5svQgEC8xnpaGW4rd3qhkNbPD/tISZwD5P3d3+i+qTGt3n5Hj7KTSWYoYk5KL6Y/Qtipa2NNCqaIyF1naZXpj/yI2DjX37lAU0C2vAhwBADysjgBR/KItVmc/Wp3G69j6uSFLng4AsBW/XixTZ1GINHm4SZFuN+9Vn3aM4RjCal0tQfP61PA9a2eEHj4aRDxtfQ3P4fOHQZWeT40wfn6b8D+GX6obp5rrQppGwjMHh4zEfoZ5NNaQ==
                    </ds:Modulus>
                    <ds:Exponent>
                        AQAB
                    </ds:Exponent>
                </ds:RSAKeyValue>
            </ds:KeyValue>
        </ds:KeyInfo>
    </ds:Signature>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
    <saml:Assertion ID="id-bb6ad0b6-7cec-4a2a-b151-f8d6bb29ff0c" Version="2.0" IssueInstant="2024-05-28T19:23:17.443Z" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
        <saml:Issuer>
            https://auth.pingone.com/c761aef4-f636-402f-b29d-6cc84c13a436
        </saml:Issuer>
        <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
                <ds:Reference URI="#id-bb6ad0b6-7cec-4a2a-b151-f8d6bb29ff0c">
                    <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/2001/04/xmlenc#sha256"/>
                    <ds:DigestValue>
                        ayAvzJwqV8Nf5XH8StCZKpctPkNIfwX4rvfmbBYYVkU=
                    </ds:DigestValue>
                </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>
                HYZKndwzyjHIxB/TEiycySfdydu080a4BbYWiq7MNwrGHGe79vOg30VmOHd60WCQO8yuQgeYXiNHCoGEFV861EVGz/iLHawBnNXRyAwv0mm3p98tBe4l2gnonVgUviUGuXz4HU6b8wPTZ4QFspL/K4t0OZoKtoAj/OlIxQ4aWaWQkHTtnER/Rv5AYAfVmgf90tHq4WBxRKOMH+7nFvXRoCiUyuevtjWBVYjVF+mD8kVVLazwGNQ/71DihYUyNTXkPZXcPsXxXDgB6PNTTx3pK9GEY5C8f0uPx847QEVuWP87K4Xg+iLVio8L5a8ENIAPGpoG5/hzWghJJ+u3Dn7oMw==
            </ds:SignatureValue>
            <ds:KeyInfo>
                <ds:X509Data>
                    <ds:X509Certificate>
                        MIIDrjCCApagAwIBAgIGAY+TeKX7MA0GCSqGSIb3DQEBCwUAMIGXMQswCQYDVQQGEwJVUzEWMBQGA1UECgwNUGluZyBJZGVudGl0eTEWMBQGA1UECwwNUGluZyBJZGVudGl0eTFYMFYGA1UEAwxPUGluZ09uZSBTU08gQ2VydGlmaWNhdGUgZm9yIFdvcmtmb3JjZSBTb2x1dGlvbiBFbnZpcm9ubWVudCA4MjBiOGQ2OSBlbnZpcm9ubWVudDAeFw0yNDA1MjAwMDQ4MjhaFw0yNTA1MjAwMDQ4MjhaMIGXMQswCQYDVQQGEwJVUzEWMBQGA1UECgwNUGluZyBJZGVudGl0eTEWMBQGA1UECwwNUGluZyBJZGVudGl0eTFYMFYGA1UEAwxPUGluZ09uZSBTU08gQ2VydGlmaWNhdGUgZm9yIFdvcmtmb3JjZSBTb2x1dGlvbiBFbnZpcm9ubWVudCA4MjBiOGQ2OSBlbnZpcm9ubWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI5Xnz5FeA+5zOMYpjLxkksCT5216B9Iq8Y4ly7dQQHMAlTkAz6ONi6itrTTe+4lVKOOexGFikFnsD2zDXEXcBzP/SVxPp4H9bObL0IBAvMZ6WhluK3d6oZDWzw/7SEmcA+T93d/ovqkxrd5+R4+yk0lmKGJOSi+mP0LYqWtjTQqmiMhdZ2mV6Y/8iNg419+5QFNAtrwIcAQA8rI4AUfyiLVZnP1qdxuvY+rkhS54OALAVv14sU2dRiDR5uEmRbjfvVZ92jOEYwmpdLUHz+tTwPWtnhB4+GkQ8bX0Nz+Hzh0GVnk+NMH5+m/A/hl+qG6ea60KaRsIzB4eMxH6GeTTWkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgYYlef5BTuMYkH3MKRV5huyM36WIvak1WF05mxXLq66eXjRjwFklNb2D6Cw6Hn3U/0kHtur3tfdK4o+K6+PHkYkFPS5kYuFDSIcR+WhLxq9ZkdIRg1EM5e8Z+FOAm1ogaSsalu++WLdgw0GJRW0jNaBLn40cABTnNXSjJfCFBgoLZjdHNhwW3A3BrXqg3LCaU1vJSamQceK6okP8dMLU4TFUhcwKlYdd8Wubp/wdnP065LZvmLgpLFPKVpQKsNJXigP71oRtteM9/E5DiPvgScZCQSJ+xVKqAAG2MffmYE+UOFfNlLPW4lG/cxCYVkh8M1iYetad04tHBcL2PSQj7g==
                    </ds:X509Certificate>
                </ds:X509Data>
                <ds:KeyValue>
                    <ds:RSAKeyValue>
                        <ds:Modulus>
                            jlefPkV4D7nM4ximMvGSSwJPnbXoH0irxjiXLt1BAcwCVOQDPo42LqK2tNN77iVUo457EYWKQWewPbMNcRdwHM/9JXE+ngf1s5svQgEC8xnpaGW4rd3qhkNbPD/tISZwD5P3d3+i+qTGt3n5Hj7KTSWYoYk5KL6Y/Qtipa2NNCqaIyF1naZXpj/yI2DjX37lAU0C2vAhwBADysjgBR/KItVmc/Wp3G69j6uSFLng4AsBW/XixTZ1GINHm4SZFuN+9Vn3aM4RjCal0tQfP61PA9a2eEHj4aRDxtfQ3P4fOHQZWeT40wfn6b8D+GX6obp5rrQppGwjMHh4zEfoZ5NNaQ==
                        </ds:Modulus>
                        <ds:Exponent>
                            AQAB
                        </ds:Exponent>
                    </ds:RSAKeyValue>
                </ds:KeyValue>
            </ds:KeyInfo>
        </ds:Signature>
        <saml:Subject>
            <saml:NameID>
                ned.oleary@ssoready.com
            </saml:NameID>
            <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml:SubjectConfirmationData NotOnOrAfter="2024-05-28T19:28:17.443Z" Recipient="https://auth.ssoready.com/v1/saml/saml_conn_8ufe2hyst7s2jdovzdvd53ldm/acs" InResponseTo="saml_flow_1bkwa10x7lhmh1zvrnhnj76r3"/>
            </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Conditions NotBefore="2024-05-28T19:22:17.443Z" NotOnOrAfter="2024-05-28T19:28:17.443Z">
            <saml:AudienceRestriction>
                <saml:Audience>
                    https://auth.ssoready.com/v1/saml/saml_conn_8ufe2hyst7s2jdovzdvd53ldm
                </saml:Audience>
            </saml:AudienceRestriction>
        </saml:Conditions>
        <saml:AuthnStatement AuthnInstant="2024-05-28T19:23:17.443Z" SessionNotOnOrAfter="2024-05-28T19:28:17.443Z" SessionIndex="bc8b16dc-4397-4169-85d4-dc3cfb23a0c9">
            <saml:AuthnContext>
                <saml:AuthnContextClassRef>
                    urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
                </saml:AuthnContextClassRef>
                <saml:AuthenticatingAuthority>
                    https://auth.pingone.com/c761aef4-f636-402f-b29d-6cc84c13a436
                </saml:AuthenticatingAuthority>
            </saml:AuthnContext>
        </saml:AuthnStatement>
        <saml:AttributeStatement>
            <saml:Attribute Name="saml_subject" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
                <saml:AttributeValue xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
                    ned.oleary@ssoready.com
                </saml:AttributeValue>
            </saml:Attribute>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>

Conceptually, the <Response> just wraps up a few <Assertion> tags. An <Assertion> just wraps up claims about our user, e.g. who this is and how they authenticated with the IDP. We process the assertions to determine whether we should log in our user.

SAML <Response> messages seem simple at first glance. Conceptually, they aren’t too complicated. In practice, though, they’re pretty frustrating. You see, the IDP will sign its <Response> and will often sign each <Assertion> too. The signature just assures us that the message actually originated from the IDP and that no one has tampered with it. And SAML digital signatures are weird.

The IDP builds messages (or nested components within a message) first by piling in the data it wants you to interpret. For instance, it will create the <AttributeValue>, my email address in this case. Then it’ll wrap up the <AttributeValue> in an <Attribute> tag, and so on until it completes its <Assertion> data. Then the IDP signs the <Assertion>. It converts the <Assertion> into bytes, then signs the bytes with its private key.

Critically, it then inserts the signature back into the <Assertion>. So the <Assertion> you receive as a service provider isn’t the same <Assertion> that the IDP signed. The IDP signed a message that didn’t yet have a signature embedded inside. In order to trust the SAML message you’ve received, you need very carefully to remove the digital signature from the <Assertion>, then convert the modified <Assertion> to bytes and verify the digital signature.

Because you need to convert the <Assertion> into bytes, even the smallest error – e.g. forgetting to clear some whitespace – will cause you not to process the message properly. And given all the flexibility in SAML, this can be pretty hard. Moreover, you can accidentally expose yourself to severe security vulnerabilities.

Assuming you’ve correctly processed the <Response> signature and any <Assertion> signatures, you can proceed to log your user in. In the example I’ve given, the IDP confirms that I, ned.oleary@ssoready.com, have authenticated using PingOne. Because you have confirmed that PingOne sent the message (and because you have a pre-configured trust relationship with PingOne that entitles it to make claims about my authentication status), you can proceed to log me in.

A word of caution

I’ve omitted a lot of details here, some of which matter greatly for security reasons. Unless you find SAML personally interesting or have some professional reason to investigate deeply, I do not recommend trying to implement SAML-based login yourself. It’s just not a good use of time.

If you’re curious, we post about auth stuff on our engineering blog from time to time.

If you’re not especially curious and just want to set up SAML as soon as possible, our company makes a free, open-source service that abstracts all the SAML away. It’ll save you a lot of time and grief.