Skip to content

8.4.5 — SAML

A customer's employees cannot log in. Your application says Invalid assertion. Their identity team says "SAML is configured correctly". There are no useful logs on either side, because the whole exchange happens in the user's browser and neither server saw the other.

This is the normal SAML experience, and it is why this page spends as much time on debugging as on the specification. SAML is verbose, old, awkward — and it is what runs enterprise single sign-on, so a backend engineer selling to businesses will integrate it.

1. What it is and why it survives

SAML 2.0 (Security Assertion Markup Language, OASIS, 2005) predates OAuth by years. It was designed for one job: let an employee authenticate once at their company's identity system and then access many separate applications, including applications run by other companies.

The vocabulary:

  • Identity provider (IdP) — the company's identity system. Okta, Microsoft Entra ID, Ping, ADFS, Google Workspace.
  • Service provider (SP) — your application.
  • Principal — the user.

It survives because enterprises already run it. Every large organisation has an identity provider, an established process for onboarding applications, and auditors who know the terminology. If you sell to enterprises, "do you support SAML" is a purchasing question, and answering no removes you from consideration regardless of your OIDC support.

For anything new, prefer OIDC (Chapter 8.4.4): JSON instead of XML, no canonicalisation problems, mobile-friendly, and vastly simpler to implement correctly. Chapter 8.4.6 draws the line.

2. The assertion

The assertion is the document the identity provider signs, saying who the user is.

xml
<saml:Assertion ID="_a1b2c3" IssueInstant="2026-08-02T14:22:10Z">
  <saml:Issuer>https://idp.customer.com/entity</saml:Issuer>          <!-- (1) -->
  <ds:Signature>…</ds:Signature>                                       <!-- (2) -->
  <saml:Subject>
    <saml:NameID Format="…:persistent">a8f3…</saml:NameID>             <!-- (3) -->
    <saml:SubjectConfirmation Method="…:bearer">
      <saml:SubjectConfirmationData
        Recipient="https://app.example.com/saml/acs"                   <!-- (4) -->
        NotOnOrAfter="2026-08-02T14:27:10Z"
        InResponseTo="_req789"/>                                       <!-- (5) -->
    </saml:SubjectConfirmation>
  </saml:Subject>
  <saml:Conditions NotBefore="2026-08-02T14:22:05Z"
                   NotOnOrAfter="2026-08-02T14:27:10Z">
    <saml:AudienceRestriction>
      <saml:Audience>https://app.example.com/metadata</saml:Audience>  <!-- (6) -->
    </saml:AudienceRestriction>
  </saml:Conditions>
  <saml:AuthnStatement AuthnInstant="2026-08-02T14:22:08Z"/>           <!-- (7) -->
  <saml:AttributeStatement>
    <saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress">
      <saml:AttributeValue>ana@customer.com</saml:AttributeValue>      <!-- (8) -->
    </saml:Attribute>
  </saml:AttributeStatement>
</saml:Assertion>

(1) Who issued it — must match the configured identity provider exactly. (2) The XML signature, covered in section 5. (3) The user's identifier in a stated format. (4) Recipient must equal your ACS URL — this binds the assertion to your endpoint. (5) InResponseTo links it to a request you made, which is what prevents replay. (6) Audience must be your entity id — the SAML equivalent of aud. (7) When authentication actually happened. (8) Attributes, whose names are long URIs and rarely match what you expected.

Three statement types exist: authentication (this user authenticated at this time, this way), attribute (facts about them), and authorization decision (deprecated and effectively unused — authorisation is done by the application).

3. How the exchange travels

Everything moves through the user's browser, which is why neither server logs the other's messages.

HTTP-Redirect binding — the message is deflated, base64-encoded and put in a query parameter. Used for the request, because it is short.

HTTP-POST binding — the message is base64-encoded into a hidden form field and auto-submitted by JavaScript. Used for the response, because assertions are far too large for a URL.

Artifact binding — the browser carries only a reference, and the service provider fetches the assertion over a back channel. More secure and rarely used, because it requires the two servers to talk directly.

SP-initiated flow, the one to prefer:

  1. User visits app.example.com, is not logged in.
  2. Your application builds an AuthnRequest with a unique ID, stores it in the session, and redirects the browser to the identity provider.
  3. The identity provider authenticates the user.
  4. It POSTs a Response containing the signed assertion to your ACS (Assertion Consumer Service) URL.
  5. You validate it, create your session, and send the user to where they were going — carried through the flow in the RelayState parameter.

IdP-initiated flow: the user clicks a tile in their company's portal, and an assertion arrives at your ACS with no request from you.

SP-initiated is safer, and the reason is InResponseTo. An unsolicited assertion has nothing to bind it to a session you started, so a captured assertion can be replayed into a victim's browser to log them in as the attacker — a login CSRF that can, for example, cause the victim to save data into the attacker's account. Support IdP-initiated only if the customer requires it, and when you do, keep a replay cache of assertion IDs and enforce the time window tightly.

4. Metadata

Configuration is exchanged as a metadata XML document, and this is the setup step.

The identity provider's metadata gives its entity id, its SSO endpoint URLs, and its signing certificates.

Your metadata gives your entity id, your ACS URL, whether you want assertions signed and encrypted, and your certificate if you sign requests or decrypt assertions.

Publish your metadata at a URL rather than emailing a file. Metadata changes — certificates rotate — and a URL lets the other side refetch. Half of SAML integrations are done by emailing XML back and forth, which is how expired certificates become outages.

Metadata can hold several certificates, and that is exactly how rotation should work: publish the new one alongside the old, let the other side pick up both, switch signing, then remove the old.

5. Signatures, and the attacks XML makes possible

XML signatures are much harder than JWT signatures, for a structural reason: XML has many byte-level representations of the same document. Attribute order, whitespace, namespace prefixes. So before signing, the document is put through canonicalisation (c14n) to produce one standard form. Any tooling that reformats XML in transit breaks the signature.

What gets signed is a choice, and it matters:

  • The assertion only — the common case.
  • The response only — weaker on its own.
  • Both — strongest.

XML signature wrapping is the classic SAML attack. The attacker takes a validly signed assertion, wraps it somewhere else in the document, and inserts a forged assertion where the parser will look. A naive implementation verifies the signature (which is valid, over the original) and then reads the other assertion. Signature check passes, wrong data used.

The defence is a rule, not a check: verify the signature and then use exactly the element the signature covers — resolve the signature's reference and process that node, rather than searching the document for an assertion. This is why every guide says not to write your own SAML library, and it is a genuinely good reason.

Encryption is separate from signing. EncryptedAssertion encrypts the assertion to the service provider's public key, so attributes are hidden from the browser carrying them. Worth requiring when attributes are sensitive; not a substitute for a signature.

Validation checklist — every item has caused a real breach or outage:

  1. Signature verifies against the certificate in the configured metadata (not one embedded in the message).
  2. The signed element is the element you use.
  3. Issuer matches the configured identity provider.
  4. Audience matches your entity id.
  5. Recipient matches your ACS URL.
  6. NotBefore / NotOnOrAfter are current, with small clock tolerance.
  7. InResponseTo matches a request in this session (SP-initiated).
  8. The assertion ID has not been seen before — keep a replay cache until the window expires.

Never trust a certificate embedded in the incoming message. An attacker can sign with their own key and attach the matching certificate. Trust only what came from metadata you configured.

6. NameID and attributes

The NameID is the user identifier, and its Format decides its behaviour:

  • persistent — a stable opaque value per user per service provider. The right choice: stable across name and email changes, and it does not leak identity across applications.
  • transient — a new value every session. Only for anonymous use.
  • emailAddress — convenient and fragile, for the same reason as Chapter 8.4.4: email addresses change and are reassigned.
  • unspecified — the identity provider decides, so you cannot rely on anything.

Attribute names are a recurring integration problem. One identity provider sends email, another sends http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, another sends urn:oid:0.9.2342.19200300.100.1.3. Make attribute mapping configurable per customer, and log the attribute names actually received — that one log line resolves a large share of onboarding tickets.

Group and role attributes are typically multi-valued, and a user in fifty groups produces a large assertion. Some identity providers cap the number sent, which produces the confusing symptom of a user losing permissions after joining more groups.

7. Single Logout, honestly

SLO propagates logout: the user logs out of one application and the identity provider notifies every other one.

It works poorly in practice. It is usually front-channel, so it depends on chained browser redirects through every service provider — and one participant that is slow, down, or blocked by third-party cookie restrictions breaks the chain silently. Implementations disagree on whether the logout request must be signed. Many large providers advise against relying on it.

Be honest about it in an interview. Support it if a customer requires it, and do not present it as a working global logout. Short session lifetimes provide more real security than a logout mechanism that fails silently.

8. Debugging

The tools: a browser SAML tracer extension captures the messages the browser carried; then base64-decode (and inflate, for redirect binding) and read the XML.

The failures, in the order you should check them:

SymptomCause
Invalid audienceAudience ≠ your entity id — usually a trailing slash or http vs https
Invalid signatureCertificate rotated, or something reformatted the XML
Assertion expiredClock skew. Both machines must run NTP; five minutes of drift breaks a five-minute window
Invalid ACS URLRecipient ≠ configured ACS — again, exact string
Login works, user has no attributesAttribute names differ from what you map
Works for some customers onlyTheir identity provider signs the response, not the assertion, or vice versa
Stopped working overnightCertificate expiry

Certificate expiry is the number one SAML operational failure. Signing certificates are often valid for years, so nobody monitors them, and the outage is total and affects every user of that customer. Monitor expiry on every configured identity provider certificate and alert 30 days ahead, and support multiple certificates in metadata so rotation is not a cutover.

Entity ids are opaque strings, not URLs to fetch. https://app.example.com/metadata is an identifier; nothing dereferences it, and it must match character for character. A trailing slash difference is a genuinely common half-day of debugging.

What the interviewer will push on

"What is SAML and when would you choose it?" An XML-based SSO protocol from 2005 where an identity provider signs an assertion about a user and the browser carries it to a service provider. Choose it because enterprise customers require it; choose OIDC for anything new. Saying that plainly is better than defending it.

"SP-initiated or IdP-initiated?" SP-initiated, because InResponseTo binds the assertion to a request you made. IdP-initiated is unsolicited, so a captured assertion can be replayed into a victim's browser and log them in as the attacker. Support it only when required, with a replay cache and a tight window.

"How do you validate an assertion?" Signature against the configured metadata certificate, use exactly the element the signature covers, then issuer, audience, recipient, time window, InResponseTo, and a replay cache on the assertion ID. The second item is the one that shows you know about signature wrapping.

"What is XML signature wrapping?" The attacker keeps a validly signed assertion but relocates it, inserting a forged one where the parser looks. The signature verifies and the wrong data is used. The defence is processing the signed node itself rather than searching for an assertion — and this is the strongest reason not to write your own implementation.

"A customer's SAML login broke overnight with no deploy. What happened?" Almost certainly a signing certificate expired or rotated. Then give the prevention: fetch metadata from a URL rather than an emailed file, support multiple certificates so rotation overlaps, and alert 30 days before expiry on every configured certificate.

"Does Single Logout work?" Not reliably. It depends on chained front-channel redirects through every service provider, and one slow or blocked participant breaks it silently — which third-party cookie restrictions make worse. Short sessions are the more honest control.

One thing to volunteer: mention logging the attribute names actually received during onboarding. Identity providers send email, a long schemas URI, or an OID for the same field, and one log line turns a multi-day integration ticket into a five-minute configuration change. It is the detail that shows you have onboarded real customers.

Recall

  • SAML 2.0: the identity provider signs an assertion about the user and the browser carries it to the service provider — which is why neither server logs the other. Use it because enterprises require it; use OIDC for anything new.
  • Assertion parts that must be checked: Issuer, Audience (your entity id), Recipient (your ACS URL), NotBefore/NotOnOrAfter, and InResponseTo.
  • Bindings: HTTP-Redirect (deflated into a query string, for requests), HTTP-POST (auto-submitted form, for assertions), artifact (back channel, rare).
  • SP-initiated is safer than IdP-initiated, because an unsolicited assertion has no InResponseTo and can be replayed to log a victim in as the attacker.
  • XML needs canonicalisation before signing, and XML signature wrapping relocates a validly signed assertion so a naive parser reads a forged one. Verify, then use exactly the signed element. Never trust a certificate embedded in the message.
  • Prefer NameID format persistent — stable and per-service-provider. Email-based identifiers change and get reassigned.
  • Attribute names differ per identity provider (email, a schemas URI, an OID). Make mapping configurable and log what actually arrived.
  • Certificate expiry is the top operational failure: publish metadata at a URL, support multiple certificates for overlapping rotation, and alert 30 days ahead. Clock skew is the second — run NTP. SLO is front-channel and fails silently; short sessions are the honest control.

Self-test: Why does neither server see the other's messages? · What does InResponseTo prevent, and what lacks it? · Describe signature wrapping and the one-line defence · Why is emailAddress a poor NameID format? · Which two configuration values fail on a trailing slash? · What breaks a SAML login overnight with no deploy?

Next: 8.4.6 puts the protocols together into the thing customers actually buy — end-to-end SSO, federation, user provisioning with SCIM, and the decision between SAML and OIDC.