Skip to content
Start with Identity
Protocols · Advanced

How SAML 2.0 Works, Message by Message

By SWI Community Team · Updated 2026-11-05 · 14 min

A SAML 2.0 login is two XML messages carried by the browser: an <AuthnRequest> from the service provider (SP) to the identity provider (IdP), and a <Response> with a signed <Assertion> coming back. Almost every SAML breach is a failure to check one field of that second message. This page walks through both messages with trimmed, realistic examples, ties each field to its section in the OASIS specifications, lists the checks an SP must run, and links each common mistake to a real CVE.

For what SAML is, when to choose it over OIDC and its status, see the SAML 2.0 standard page. This page assumes you know the roles and want the wire-level detail.

Sources. Section numbers refer to the OASIS SAML 2.0 Assertions and Protocols (Core), Bindings and Profiles specifications (15 March 2005), plus the approved errata. The flow described is the Web Browser SSO profile, Profiles §4.1.

The sequence at a glance

# From → to What travels Binding Spec
1 Browser → SP GET https://app.example.com/reports (no session) HTTP Profiles §4.1.3.1
2 SP → browser → IdP <AuthnRequest> in the SAMLRequest query parameter, plus RelayState HTTP-Redirect Core §3.4.1, Bindings §3.4
3 IdP ↔ user Login, MFA, conditional access (out of scope for SAML) none Profiles §4.1.3.4
4 IdP → browser → SP <Response> with <Assertion> in the SAMLResponse form field HTTP-POST Core §3.2.2 and §2.3.3, Bindings §3.5
5 SP Validates, creates its own session, redirects to RelayState none Profiles §4.1.4.3

Steps 2 and 4 are the only SAML messages. The Artifact binding (Bindings §3.6) swaps step 4 for a reference the SP resolves over a back channel, which is rarer in SaaS.

Message 1: the AuthnRequest

The SP builds this XML, deflates it, base64-encodes it and URL-encodes it into a redirect (Bindings §3.4.4.1).

<samlp:AuthnRequest
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_a1b2c3d4e5f60718293a4b5c6d7e8f90"
    Version="2.0"
    IssueInstant="2026-11-05T14:02:11Z"
    Destination="https://idp.example.com/saml2/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:AuthnRequest>

The redirect the browser follows looks like this (values shortened):

GET /saml2/sso?SAMLRequest=fZJNT8MwDIbvSPyHKPe2a8cOi9ZOAzQJiY9pA4S4hdR0EWlS4rSFf0%2B6DQkO7GTLfu3Hr53Z...
  &RelayState=%2Freports
  &SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256
  &Signature=Qm9ndXNTaWduYXR1cmVGb3JFeGFtcGxlT25seQ%3D%3D HTTP/1.1
Host: idp.example.com
Field Meaning Spec
ID Unique request identifier. Must be an xs:ID, so it cannot start with a digit; implementations prefix an underscore. The SP stores it to match InResponseTo later. Core §1.3.4, §3.2.1
Version, IssueInstant Always 2.0; UTC timestamp of issue. Core §3.2.1
Destination The IdP endpoint the request is sent to. Required to be checked when the message is signed. Core §3.2.1, Bindings §3.4.5.2
AssertionConsumerServiceURL Where the SP wants the Response delivered. The IdP must check it against SP metadata, not trust it blindly. Core §3.4.1, Profiles §4.1.4.1
ProtocolBinding How the Response should come back, here HTTP-POST. Core §3.4.1
<Issuer> The SP's entity ID, which becomes the expected <Audience>. Core §2.2.5, Profiles §4.1.4.1
<NameIDPolicy> Requested identifier format: persistent (opaque, stable, pairwise) or emailAddress. Core §3.4.1.1, §8.3
RequestedAuthnContext (optional) Asks for a class of authentication, for example PasswordProtectedTransport or a phishing-resistant class your IdP defines. Core §3.3.2.2.1
RelayState Opaque value the IdP returns unchanged. Use it for the post-login path, never as an unvalidated redirect target. Bindings §3.4.3
SigAlg, Signature With the Redirect binding the signature is over the query string, not inside the XML. Bindings §3.4.4.1

Many SPs leave the AuthnRequest unsigned. That is allowed, but signing it stops anyone from forging requests that redirect assertions to an unexpected ACS URL, which is why IdPs increasingly let you require it.

Message 2: the Response and Assertion

The IdP returns an HTML page with an auto-submitting form whose SAMLResponse field is the base64 of the XML below (Bindings §3.5.4). Signature values and certificates are trimmed.

<samlp:Response
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_resp9f8e7d6c5b4a39281706f5e4d3c2b1a0"
    InResponseTo="_a1b2c3d4e5f60718293a4b5c6d7e8f90"
    Version="2.0"
    IssueInstant="2026-11-05T14:02:19Z"
    Destination="https://app.example.com/saml/acs">
  <saml:Issuer>https://idp.example.com/saml2</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion ID="_assert0a1b2c3d4e5f67890abcdef1234567" Version="2.0"
                  IssueInstant="2026-11-05T14:02:19Z">
    <saml:Issuer>https://idp.example.com/saml2</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="#_assert0a1b2c3d4e5f67890abcdef1234567">
          <!-- Transforms: enveloped-signature, exc-c14n -->
          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
          <ds:DigestValue>RXhhbXBsZURpZ2VzdE5vdFJlYWw=</ds:DigestValue>
        </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue>RXhhbXBsZVNpZ25hdHVyZQ==</ds:SignatureValue>
    </ds:Signature>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
        u-7f3c9a21-example
      </saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData
            InResponseTo="_a1b2c3d4e5f60718293a4b5c6d7e8f90"
            Recipient="https://app.example.com/saml/acs"
            NotOnOrAfter="2026-11-05T14:07:19Z"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2026-11-05T14:01:49Z"
                     NotOnOrAfter="2026-11-05T14:07:19Z">
      <saml:AudienceRestriction>
        <saml:Audience>https://app.example.com/saml/metadata</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2026-11-05T14:02:17Z"
                         SessionIndex="_sess-31d7-example"
                         SessionNotOnOrAfter="2026-11-05T22:02:17Z">
      <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>ada@example.com</saml:AttributeValue></saml:Attribute>
      <saml:Attribute Name="groups"><saml:AttributeValue>finance-readers</saml:AttributeValue></saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

Response envelope

Field Meaning Spec
InResponseTo The ID of the AuthnRequest this answers. Absent in IdP-initiated (unsolicited) logins. Core §3.2.2, Profiles §4.1.5
Destination Must equal the ACS URL where it arrived whenever the Response is signed. Core §3.2.2, Bindings §3.5.5.2
<Status> / <StatusCode> Success or an error such as Requester, Responder, AuthnFailed. On error, no assertion may be included. Core §3.2.2.1–3.2.2.2, Profiles §4.1.4.2
<Issuer> Optional on the Response; if present it must be the IdP entity ID. Profiles §4.1.4.2

Assertion

Field Meaning Spec
<Assertion ID> Target of the signature reference (URI="#..."), and the key for your replay cache. Core §2.3.3, §5.4.2
<ds:Signature> Enveloped XML Signature with exclusive canonicalization. Must reference the ID of the element being signed, with a single <ds:Reference>. Core §5.4.1–5.4.4
<Issuer> IdP entity ID. Must match the IdP you configured. Core §2.2.5, Profiles §4.1.4.2
<NameID> The subject identifier. persistent is an opaque per-SP value; emailAddress is readable but can be reassigned. Key accounts on Issuer + NameID. Core §2.2.3, §8.3.2, §8.3.7
<SubjectConfirmation Method="...:cm:bearer"> Whoever presents this assertion is the subject. That is why every other check matters. Core §2.4.1.1, Profiles §4.1.4.2
<SubjectConfirmationData> Recipient (your ACS URL), NotOnOrAfter (delivery window), InResponseTo. Must not carry NotBefore in this profile. Core §2.4.1.2, Profiles §4.1.4.2
<Conditions NotBefore NotOnOrAfter> Validity window of the assertion itself. Core §2.5.1.2
<AudienceRestriction> / <Audience> Your SP entity ID. An assertion for another SP must fail here. Core §2.5.1.4
<OneTimeUse> (optional) Asks the SP not to cache the assertion for reuse. Core §2.5.1.5
<AuthnStatement> When and how the user authenticated. SessionIndex is needed for Single Logout; SessionNotOnOrAfter caps the SP session. Core §2.7.2, Profiles §4.1.4.3
<AuthnContextClassRef> Authentication class, defined in the Authentication Context specification. Core §2.7.2.2
<AttributeStatement> Profile attributes and group claims, often used for just-in-time provisioning and role mapping. Core §2.7.3
<EncryptedAssertion> (alternative) The whole assertion encrypted to the SP's key. Decrypt first, then apply every check below to the decrypted assertion. Core §2.3.4, §6

The eight checks the SP must run

Profiles §4.1.4.3 and §4.1.4.5 set the minimum; Core and Bindings add the rest. Run all of them, and reject on the first failure.

  1. Verify the signature with the IdP's key from metadata, and read only what was signed. With the POST binding the assertion must be signed (Profiles §4.1.4.5). Take the key from configured metadata, never from a <KeyInfo> inside the message, and make sure the element your code extracts NameID and attributes from is the exact element the <ds:Reference> pointed to (Core §5.4.2).
  2. Issuer on the assertion equals the configured IdP entity ID (Profiles §4.1.4.2).
  3. Audience contains your SP entity ID (Core §2.5.1.4).
  4. Recipient in the bearer SubjectConfirmationData equals the ACS URL that received the message (Profiles §4.1.4.3), and Destination on a signed Response equals it too (Bindings §3.5.5.2).
  5. Time windows. SubjectConfirmationData NotOnOrAfter has not passed, and the assertion's Conditions NotBefore/NotOnOrAfter bracket now, with a small clock-skew allowance (Profiles §4.1.4.3, Core §2.5.1.2).
  6. InResponseTo equals the ID of an AuthnRequest you issued in this browser session and have not already consumed. For unsolicited responses it must be absent, and you should decide whether to accept IdP-initiated login at all (Profiles §4.1.4.3, §4.1.5).
  7. Replay cache. Store each assertion ID until its NotOnOrAfter and reject repeats (Profiles §4.1.4.5).
  8. Structure. Status is Success, there is exactly the assertion you expect (reject extra or nested assertions), and any condition you do not understand makes the assertion invalid (Core §2.5.1.1).

Common implementation mistakes, with the CVEs they produced

Mistake What goes wrong Real examples
Signature verified on one node, data read from another (signature wrapping) An attacker with any validly signed assertion wraps a forged one around or beside it and logs in as anyone CVE-2024-45409 (ruby-saml), CVE-2025-47949 (samlify), CVE-2024-6202 (HaloITSM), CVE-2024-6800 (GitHub Enterprise Server)
Two XML parsers disagree about the same bytes The signature library and the data reader see different documents CVE-2025-25291, CVE-2025-25292, CVE-2025-66567, CVE-2025-23369 (libxml2 canonicalization in GHES)
Checks skipped on the encrypted-assertion path Validation runs before decryption, or not at all after it CVE-2024-4985, CVE-2024-9487
No replay cache, or replay checked only at one layer A captured assertion opens a second session CVE-2026-18967 (Keycloak SAML broker)
Trust not tied to a specific IdP or domain Assertions from a disabled or different IdP still accepted CVE-2026-2603, CVE-2024-20355
ACS URL from the request reflected without validation XSS or assertion delivery to the wrong endpoint CVE-2023-6717
Appliance SSO code paths that trust crafted messages Pre-auth bypass on the network edge CVE-2025-59718, CVE-2026-19490

Two attacks need no SP bug at all. If the IdP's signing key is stolen, every SP accepts forged assertions: that is federation trust abuse and SAML forgery. And a real user completing a real login through a reverse proxy hands over the SP session cookie whatever the protocol: see adversary-in-the-middle phishing and session cookie theft.

The fix for the first table is the same every time: use a maintained SAML library, pin it, patch it within days, and configure it to require signed assertions, reject unsolicited responses unless you need them, and use a replay cache.

Try it

Paste a SAMLRequest or SAMLResponse into the SAML decoder to inflate, decode and pretty-print it in your browser, then compare each field to the tables above. For a live test of your SP, take one valid response and change one thing at a time: the Audience, the Recipient, one character of a signed attribute, the InResponseTo. Each variant must be rejected.

SAML 2.0 standard page, SAML vs OIDC, how OIDC works, message by message, implementing SSO, identity federation implementation. Glossary: SAML, identity provider, service provider, SSO identifier, SSO enforcement.

Frequently asked questions

What messages are exchanged in a SAML 2.0 login?
In the common SP-initiated Web Browser SSO profile there are two SAML messages. The service provider sends an AuthnRequest to the identity provider, usually through the HTTP-Redirect binding, and the identity provider returns a Response containing a signed Assertion, usually through the HTTP-POST binding to the SP's Assertion Consumer Service URL. Everything else is ordinary HTTP: the redirect, the login page and the auto-submitting form.
Which SAML fields must a service provider validate?
At minimum: the XML signature over the assertion you read, the Issuer, the Audience in AudienceRestriction, the Recipient in the bearer SubjectConfirmationData, NotOnOrAfter and NotBefore with small clock skew, InResponseTo against an AuthnRequest ID you issued, Destination on a signed Response, and a replay cache of assertion IDs. These come from SAML Core sections 2.4 and 2.5, Profiles section 4.1.4, and Bindings sections 3.4.5.2 and 3.5.5.2.
Why do SAML signature wrapping attacks keep happening?
Because the signature check and the data extraction often run on different views of the same XML document. The signature covers an element identified by its ID attribute, but the code that reads NameID and attributes may select a different, unsigned element. Parser differentials, where two XML parsers disagree about the same bytes, make that gap easier to exploit. The ruby-saml and samlify CVEs of 2024 and 2025 are all variations of this.
How do I decode a SAMLResponse?
A SAMLResponse sent with the HTTP-POST binding is plain base64 of the XML. A SAMLRequest sent with the HTTP-Redirect binding is DEFLATE-compressed, then base64-encoded, then URL-encoded, so you reverse those three steps. The SWI SAML decoder does both in the browser without sending the message anywhere.
Last reviewed By SWI Community TeamSuggest a correctionHow we research