Skip to content
Published on

SAML 2.0 Deep Dive — Mastering Assertions, Bindings, and Metadata

Authors

Introduction

Even in 2026, when OIDC is the default for new builds, SAML 2.0 remains the lingua franca on the ground in enterprise B2B SSO. When the requirement "integrate with the customer Entra ID, Okta, or self-hosted IdP" lands on your desk, more often than not a SAML metadata XML file arrives with it. The reason a protocol standardized in 2005 has stayed in active service for over 20 years is simple: the inertia of already-deployed trust relationships and the simple premise that only a browser is required.

The problem is that SAML tends to be treated as a "configure-and-forget black box" — until an outage hits and nobody knows what is inside. This article dissects the core of SAML 2.0 — Assertions, the Protocol (AuthnRequest/Response), Bindings, and Metadata — at the actual XML level, and covers attacks and defenses such as XML Signature Wrapping plus operational issues such as clock skew.

The Four-Layer Structure of SAML 2.0

The SAML specification consists of four layers. Knowing this split makes the spec documents far easier to read.

+-----------------------------------------------------------+
| Profiles   : usage scenarios combining the layers          |
|              (Web Browser SSO Profile, Single Logout, ...) |
+-----------------------------------------------------------+
| Bindings   : transport methods that carry the messages     |
|              (HTTP-Redirect, HTTP-POST, Artifact, SOAP)    |
+-----------------------------------------------------------+
| Protocols  : the format of request/response messages       |
|              (AuthnRequest, Response, LogoutRequest, ...)  |
+-----------------------------------------------------------+
| Assertions : the identity information itself               |
|              (AuthnStatement, AttributeStatement, ...)     |
+-----------------------------------------------------------+
  • Assertion: an XML document stating "this user is so-and-so, was authenticated at this time in this way, and has these attributes".
  • Protocol: the message formats for requesting and returning Assertions.
  • Binding: the rules for carrying those messages over HTTP.
  • Profile: the bundle of all three into a complete scenario called "Web Browser SSO".

What we usually call "a SAML integration" is almost always the Web Browser SSO Profile.

Dissecting the Assertion — The XML of Identity Statements

Below is the skeleton of an Assertion issued by a real IdP (signature omitted).

<saml:Assertion
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_a1b2c3d4e5f6"
    Version="2.0"
    IssueInstant="2026-06-12T09:30:00Z">

  <saml:Issuer>https://idp.corp.com/saml</saml:Issuer>

  <saml:Subject>
    <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
      f9a8b7c6-1234-5678-90ab-cdef12345678
    </saml:NameID>
    <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
      <saml:SubjectConfirmationData
          NotOnOrAfter="2026-06-12T09:35:00Z"
          Recipient="https://app.example.com/saml/acs"
          InResponseTo="_req-98765"/>
    </saml:SubjectConfirmation>
  </saml:Subject>

  <saml:Conditions
      NotBefore="2026-06-12T09:29:00Z"
      NotOnOrAfter="2026-06-12T09:35:00Z">
    <saml:AudienceRestriction>
      <saml:Audience>https://app.example.com/saml/metadata</saml:Audience>
    </saml:AudienceRestriction>
  </saml:Conditions>

  <saml:AuthnStatement
      AuthnInstant="2026-06-12T09:30:00Z"
      SessionIndex="_sess-112233">
    <saml:AuthnContext>
      <saml:AuthnContextClassRef>
        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
      </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>

  <saml:AttributeStatement>
    <saml:Attribute Name="email">
      <saml:AttributeValue>yj.kim@corp.com</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="department">
      <saml:AttributeValue>Platform Engineering</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="groups">
      <saml:AttributeValue>sso-admins</saml:AttributeValue>
      <saml:AttributeValue>developers</saml:AttributeValue>
    </saml:Attribute>
  </saml:AttributeStatement>
</saml:Assertion>

Meaning of Each Element and Validation Points

ElementMeaningWhat the SP must validate
IssuerentityID of the issuing IdPExactly matches the entityID of a trusted IdP
Subject/NameIDUser identifierFormat is the agreed one (persistent, emailAddress, etc.)
SubjectConfirmationConditions for "whoever presents this Assertion"Recipient is my ACS URL, NotOnOrAfter still valid, InResponseTo matches a request ID I issued
ConditionsValidity window and intended audienceNotBefore/NotOnOrAfter window, Audience equals my entityID
AuthnStatementWhen/how authentication happenedAuthnContextClassRef satisfies policy (e.g. MFA required)
AttributeStatementUser attributesParse per mapping rules, feed into authorization

The NameID Format is a frequent source of operational pain. persistent is an opaque identifier fixed per service, transient is a one-time value that changes per session, and emailAddress is a human-readable email. If the SP expects emailAddress but the IdP sends persistent, you get the classic "login works but account matching fails" outage. Agree on it explicitly at integration kickoff.

The AuthnRequest / Response Flow

SP-initiated SSO (the standard path)

This is the case where the user reaches the SP first. The SP builds an AuthnRequest and sends the browser to the IdP.

<samlp:AuthnRequest
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_req-98765"
    Version="2.0"
    IssueInstant="2026-06-12T09:29:50Z"
    Destination="https://idp.corp.com/saml/sso"
    AssertionConsumerServiceURL="https://app.example.com/saml/acs"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
  <saml:Issuer>https://app.example.com/saml/metadata</saml:Issuer>
  <samlp:NameIDPolicy
      Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
      AllowCreate="true"/>
  <samlp:RequestedAuthnContext Comparison="minimum">
    <saml:AuthnContextClassRef>
      urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
    </saml:AuthnContextClassRef>
  </samlp:RequestedAuthnContext>
</samlp:AuthnRequest>

After authentication, the IdP returns a Response. The Assertion lives inside the Response.

<samlp:Response
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="_resp-55555"
    Version="2.0"
    IssueInstant="2026-06-12T09:30:00Z"
    Destination="https://app.example.com/saml/acs"
    InResponseTo="_req-98765">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
    https://idp.corp.com/saml
  </saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <!-- the signed saml:Assertion goes here -->
</samlp:Response>

The key correspondence: the ID of the AuthnRequest must match the InResponseTo of the Response, and the InResponseTo inside the SubjectConfirmationData of the Assertion. Skipping this validation enables attacks that inject a Response intended for a different session.

SP-initiated vs IdP-initiated

SP-initiated (recommended)               IdP-initiated
--------------------                     --------------------
User -> reaches the SP                   User -> reaches the IdP portal
SP creates AuthnRequest (records ID)     When the user clicks an app tile,
IdP authenticates, sends Response        the IdP issues an Unsolicited
  (InResponseTo = request ID)            Response with no AuthnRequest
SP: can validate InResponseTo            SP: cannot validate InResponseTo
                                         (there was no request at all)
CSRF/injection defense is easy           Comparatively exposed to
                                         Response injection

IdP-initiated SSO is commonly demanded in enterprises for the "click an app icon in the corporate portal" UX, but it is the security underdog because InResponseTo validation is impossible. The best practice, where feasible, is to point the IdP portal app tile at the login-start URL of the SP, effectively converting the flow into SP-initiated.

Bindings — Three Ways to Carry the Messages

HTTP-Redirect Binding

Used for small messages such as the AuthnRequest. The message is DEFLATE-compressed, base64-encoded, then URL-encoded into the query string.

GET /saml/sso?SAMLRequest=fZJNb9swDIb%2FisG7...&RelayState=abc123
    &SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256
    &Signature=KJh8... HTTP/1.1
Host: idp.corp.com
  • Unsuitable for large messages (a signed Response) due to URL length limits.
  • The signature travels not inside the XML but as query parameters (SigAlg, Signature) — a detached signature.

HTTP-POST Binding

Used for large messages such as the Response. The IdP returns an auto-submitting HTML form, and the browser POSTs it to the ACS of the SP.

<form method="post" action="https://app.example.com/saml/acs">
  <input type="hidden" name="SAMLResponse" value="PHNhbWxwOlJlc3BvbnNlIC4uLg=="/>
  <input type="hidden" name="RelayState" value="abc123"/>
</form>
<script>document.forms[0].submit();</script>
  • Only base64 is applied (no compression); the signature is embedded inside the XML.
  • Practically every Web SSO integration delivers its Response over this binding.

HTTP-Artifact Binding

For cases where sensitive content must not pass through the browser. Only a short reference value (the artifact) goes to the browser; the SP fetches the real message from the IdP over a back channel (SOAP).

[Browser]            [SP]                       [IdP]
    |<-- artifact ------|                           |
    |--- artifact ----->|                           |
    |                   |--- ArtifactResolve(SOAP)->|
    |                   |<-- ArtifactResponse ------|
    |                   |    (the real SAMLResponse)|
  • High security, but it requires direct SP-to-IdP network connectivity and is complex to implement, so it is rare in practice.

Binding Comparison

AspectHTTP-RedirectHTTP-POSTHTTP-Artifact
UseAuthnRequest, LogoutRequestResponse deliveryHigh-security environments
EncodingDEFLATE + base64 + URLbase64artifact reference
Signature locationQuery parameters (detached)Inside XML (enveloped)Inside XML
Size limitURL length limits applyPractically noneNot applicable
Back channel neededNoNoYes (SOAP)
Real-world frequencyHigh (requests)Very high (responses)Low

Metadata — The Configuration File of Trust

Every SAML integration starts with metadata exchange. An example of SP metadata:

<md:EntityDescriptor
    xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
    entityID="https://app.example.com/saml/metadata">
  <md:SPSSODescriptor
      AuthnRequestsSigned="true"
      WantAssertionsSigned="true"
      protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

    <md:KeyDescriptor use="signing">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>MIIDdzCCAl+gAwIBAgIE...</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>

    <md:SingleLogoutService
        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        Location="https://app.example.com/saml/slo"/>

    <md:AssertionConsumerService
        index="0" isDefault="true"
        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        Location="https://app.example.com/saml/acs"/>
  </md:SPSSODescriptor>
</md:EntityDescriptor>

IdP metadata contains the SingleSignOnService endpoints and the signing certificate of the IdP. Operational points:

  • The entityID is an identifier, not a URL. By convention it looks like a URL, but exact string equality is all that matters. A single trailing slash difference breaks the integration.
  • Certificate expiry is the number-one cause of SAML outages. The certificate inside the metadata expires independently of your TLS certificate. Automate alerts at 90/30/7 days before expiry.
  • Key rollover: multiple KeyDescriptor entries are allowed. Zero-downtime rotation works as: add the new certificate to metadata, confirm the peer refreshed it, switch the signing key, then remove the old certificate.
  • Sign the metadata itself, or exchange it only over trusted channels (admin console, signed URL).

XML Signature and Encryption

Signature Structure

SAML uses the enveloped signature of XML Signature (XMLDSig). A digest of the signed content is computed, and the SignedInfo containing that digest is signed with the private key.

<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="#_a1b2c3d4e5f6">
      <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>uNk8...</ds:DigestValue>
    </ds:Reference>
  </ds:SignedInfo>
  <ds:SignatureValue>KJh8a9...</ds:SignatureValue>
</ds:Signature>

Note that the Reference URI is an ID attribute reference: it means "the element whose ID is _a1b2c3d4e5f6 is what is signed". This very indirection is what gives the Signature Wrapping attack its opening.

Signing can apply to the whole Response and to each Assertion. Signing both is recommended, and Assertion signing is the bare minimum.

Encryption (EncryptedAssertion)

Because the Assertion transits the browser (front channel), if its attributes contain sensitive data you can encrypt the Assertion with XML Encryption using the public key of the SP. If signing means "tamper-proofing", encryption means "content hiding". They have different jobs — encryption does not replace signing.

The XML Signature Wrapping (XSW) Attack and Defenses

This is the most famous family of attacks in SAML history. The core idea: manipulate the XML structure so that the signature validation logic looks at a different element than the one the application actually reads.

Legitimate Response                   XSW attack Response
--------------                        ------------------
Response                              Response
 └─ Assertion (ID=A, signed)           ├─ [forged Assertion] (ID=B, unsigned)
     └─ Subject: alice                 │    └─ Subject: admin   <-- what the app reads
                                       └─ [original Assertion] (ID=A, valid signature)
                                            └─ Subject: alice   <-- what the validator sees

Signature validator: "signature of element ID=A? valid" --> pass
Application: parses the first Assertion (forged) --> logs in as admin

In the 2012 research, a majority of the 14 major SAML libraries of the day fell to this attack family, and variants keep surfacing periodically since.

Defense Checklist

  1. Do not roll your own — use the latest version of a vetted library (OpenSAML and friends) and track its security patches.
  2. Use only "the exact node that was signed" — read data only from the DOM node whose signature validated. Parsing styles like "find the first Assertion in the document" are forbidden.
  3. Schema validation first — reject structures that violate the SAML schema (duplicate Assertions, elements in odd positions) before signature validation.
  4. Enforce the Assertion count — Web SSO carries one Assertion. Reject anything with more.
  5. Require signatures on both Response and Assertion — enable WantAssertionsSigned and demand Response signing too.
  6. Harden the XML parser — disable DTDs and external entities (XXE).

RelayState — The Easily Forgotten Supporting Actor

RelayState is an opaque parameter carrying "where to return after login". In SP-initiated flows the value sent by the SP comes back untouched alongside the Response; in IdP-initiated flows the IdP may place a destination URL in it.

Operational/security points:

  • The spec imposes an 80-byte limit, so it is safer to carry a key into server-side state rather than a whole URL.
  • Using a returned RelayState in a redirect without validation creates an open redirect vulnerability. Validate with an allowlist or a signed/server-side lookup.
  • RelayState is not covered by the signature, so treat it as tamperable by default.

Clock Skew — The Usual Suspect for Intermittent Failures

NotBefore/NotOnOrAfter on the Assertion typically form a window of just a few minutes around issuance. When the IdP and SP clocks drift apart, this happens:

IdP clock: 09:30:00  -->  issues with NotBefore=09:29:00
SP clock:  09:28:30  -->  "NotBefore is in the future" --> rejected

Symptom: the same user sometimes succeeds on retry (intermittent)
         fails only when routed to a specific SP node (the one with drift)

Countermeasures:

  1. Enforce NTP/chrony on every node and monitor drift.
  2. Configure the clock skew allowance of your SAML library to 60-120 seconds (most default to 0 or very small).
  3. For incident analysis, keep logs that record "which node failed" together with the clock of that node.

Validation failure logs should contain at minimum the Issuer, InResponseTo, the rejection reason (time condition/Audience/signature), and the server time. A single-line "Invalid SAML response" log is an act of torture against your operators.

SP Implementation Validation Checklist

On receiving a SAML Response (ACS endpoint):
[ ] 1. Parse with a hardened XML parser (DTD/XXE blocked)
[ ] 2. Schema validation
[ ] 3. Verify the Response signature (if required)
[ ] 4. Confirm Status is Success
[ ] 5. Verify the Assertion signature — against the trusted IdP certificate
[ ] 6. Read all further data only from the signed node
[ ] 7. Issuer equals the expected IdP entityID
[ ] 8. Conditions: NotBefore/NotOnOrAfter (with skew allowance)
[ ] 9. AudienceRestriction equals my entityID
[ ] 10. SubjectConfirmationData: Recipient is my ACS URL,
        NotOnOrAfter valid, InResponseTo matches a request ID I issued
[ ] 11. Check Assertion ID reuse (replay prevention cache)
[ ] 12. NameID Format is the agreed format
[ ] 13. Establish the app session only after all checks pass

Why SAML Is Still Alive in 2026

  1. Inertia of B2B trust relationships — tens of thousands of corporate IdPs and SaaS products are already wired together with SAML. There is little business motivation to rip out working trust.
  2. Procurement requirements — "SAML SSO support" still appears on enterprise SaaS purchasing checklists. Even the criticism of locking SSO behind premium plans (the so-called SSO tax) is evidence of how standard a requirement SAML is.
  3. The legacy IdP ecosystem — legacy WAM products such as Broadcom SiteMinder (currently 12.9) still run inside large enterprises, and their standard integration path is SAML. Even in migrations from header-based authentication to standard protocols, SAML is often the first station.
  4. Completeness of the protocol itself — for the Web SSO use case, SAML is a finished protocol. Its lack of change is also its stability.

The direction, however, is clear: new capabilities (passkeys integration, token exchange, FAPI, etc.) all happen on the OIDC side, and modern IdPs such as Keycloak support both SAML and OIDC. The standard 2026 architecture is hybrid: IdP as the hub, legacy SPs over SAML, new apps over OIDC.

            [Keycloak 26.6 / Okta / Entra ID]
                  |                |
        SAML 2.0  |                |  OIDC
                  v                v
        [Legacy/B2B SaaS]   [New web/mobile/API]

Closing

SAML 2.0 in one sentence: "a Web SSO protocol that carries XML Assertions via browser redirects and form POSTs, with trust guaranteed by XML Signature". Three things to remember in practice:

  • Validate every one of the Assertion conditions (time, Audience, Recipient, InResponseTo) — signature verification alone is not enough.
  • The lesson of Signature Wrapping: the node whose signature validated and the node you read data from must be the same. Do not hand-roll; use a vetted library.
  • The big three of operational outages are certificate expiry, clock skew, and entityID/URL mismatches. All are preventable with monitoring and automation.

The next article covers the internals of OIDC — Authorization Code Flow, Discovery, JWKS, and token validation — at the same depth.

References