# How SAML 2.0 Works, Message by Message

Source: https://startwithidentity.com/guides/protocols/saml-message-flow/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

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](https://startwithidentity.com/standards/saml-2-0/). 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)](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf), [Bindings](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf) and [Profiles](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf) specifications (15 March 2005), plus the [approved errata](https://docs.oasis-open.org/security/saml/v2.0/sstc-saml-approved-errata-2.0.html). 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).

```xml
<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):

```http
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.

```xml
<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](https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf) 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](https://startwithidentity.com/cves/cve-2024-45409/) (ruby-saml), [CVE-2025-47949](https://startwithidentity.com/cves/cve-2025-47949/) (samlify), [CVE-2024-6202](https://startwithidentity.com/cves/cve-2024-6202/) (HaloITSM), [CVE-2024-6800](https://startwithidentity.com/cves/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](https://startwithidentity.com/cves/cve-2025-25291/), [CVE-2025-25292](https://startwithidentity.com/cves/cve-2025-25292/), [CVE-2025-66567](https://startwithidentity.com/cves/cve-2025-66567/), [CVE-2025-23369](https://startwithidentity.com/cves/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](https://startwithidentity.com/cves/cve-2024-4985/), [CVE-2024-9487](https://startwithidentity.com/cves/cve-2024-9487/) |
| No replay cache, or replay checked only at one layer | A captured assertion opens a second session | [CVE-2026-18967](https://startwithidentity.com/cves/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](https://startwithidentity.com/cves/cve-2026-2603/), [CVE-2024-20355](https://startwithidentity.com/cves/cve-2024-20355/) |
| ACS URL from the request reflected without validation | XSS or assertion delivery to the wrong endpoint | [CVE-2023-6717](https://startwithidentity.com/cves/cve-2023-6717/) |
| Appliance SSO code paths that trust crafted messages | Pre-auth bypass on the network edge | [CVE-2025-59718](https://startwithidentity.com/cves/cve-2025-59718/), [CVE-2026-19490](https://startwithidentity.com/cves/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](https://startwithidentity.com/techniques/federation-trust-abuse-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](https://startwithidentity.com/techniques/aitm-phishing-proxy/) and [session cookie theft](https://startwithidentity.com/techniques/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](https://startwithidentity.com/tools/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.

## Related

[SAML 2.0 standard page](https://startwithidentity.com/standards/saml-2-0/), [SAML vs OIDC](https://startwithidentity.com/guides/fundamentals/saml-vs-oidc/), [how OIDC works, message by message](https://startwithidentity.com/guides/protocols/oidc-message-flow/), [implementing SSO](https://startwithidentity.com/guides/authentication/complete-guide-implementing-sso/), [identity federation implementation](https://startwithidentity.com/guides/authentication/identity-federation-implementation-guide/). Glossary: [SAML](https://startwithidentity.com/glossary/saml/), [identity provider](https://startwithidentity.com/glossary/identity-provider/), [service provider](https://startwithidentity.com/glossary/service-provider/), [SSO identifier](https://startwithidentity.com/glossary/sso-identifier/), [SSO enforcement](https://startwithidentity.com/glossary/sso-enforcement/).
