SAML and the Enterprise
42.0 What this chapter gives you#
- You will be able to name the four layers of SAML — assertions, protocols, bindings and profiles — and say which OASIS document defines each one.
- You will be able to draw the Web Browser SSO flow from a blank page, in both its service-provider-initiated and identity-provider-initiated forms, and say what is missing from the second one.
- You will be able to choose between the HTTP-Redirect, HTTP-POST and Artifact bindings for a given deployment and defend the choice on size, exposure and back-channel grounds.
- You will be able to read a real SAML assertion field by field and say what each of Subject, Conditions, AuthnStatement and AttributeStatement is asserting.
- You will be able to explain the difference between signing the response and signing the assertion, and say which one a service provider must insist on and why.
- You will be able to construct an XML signature wrapping document on paper, explain exactly why the signature still verifies, and name the code pattern that makes it work.
- You will be able to describe the comment-truncation class of attack found by Duo Labs in 2018, and say why canonicalization is what makes it possible.
- You will be able to list the parser settings that stop XML external entity attacks in a SAML endpoint, and say why the order of parsing and verifying matters more than either alone.
- You will be able to run a certificate rollover across a federation without an outage, and say what SAML metadata says about certificate expiry that surprises most engineers.
- You will be able to give an honest account, as of August 2026, of where SAML still beats OpenID Connect and where it plainly does not.
Somewhere in your working life you have clicked a button that said “Sign in with your company account”, watched the screen flicker twice, and arrived inside an application you had never given a password to. If you work for a bank, a hospital, a university or a government department, there is a very good chance that the flicker you saw was SAML: an XML-based protocol whose central specifications were approved in March 2005 and have not changed since. The technical committee that produced it, the OASIS Security Services Technical Committee, was formally closed on 8 July 2023. The protocol is finished. It is also, as of August 2026, still carrying an enormous share of corporate login, though no independent measurement of that share is published [UNVERIFIED: a measured 2026 figure for SAML’s share of enterprise sign-ins].
That combination — frozen specification, live deployment — is what makes SAML worth a chapter of its own. It is not a museum piece; it is load-bearing. And yet it was designed in an era with different assumptions about XML, about browsers, and about who was likely to be attacking you.
The thesis of this chapter is a single sentence: a twenty-year-old XML protocol still carries most corporate login, and its complexity is where its vulnerabilities live. Every serious SAML break of the last two decades — the 2008 Google Apps flaw, the 2012 signature wrapping paper, the 2018 comment truncation findings, the 2025 parser differentials — comes from one root. SAML asks a computer to verify a cryptographic signature over a document, and then asks it, separately, to read facts out of that document. When those two operations disagree about which bytes they are looking at, an attacker becomes whoever they like. XML digital signatures created that gap. SAML is where it has money and staff records on the other side of it.
We will follow one login the whole way through. Ravi Menon works at a manufacturer we will call Northwind. Northwind’s identity provider lives at login.northwind.example. Ravi needs to file a travel expense claim in a hosted application we will call Voyagr, at voyagr.example. Voyagr has never seen Ravi’s password and never will. Every value in this chapter — assertion identifiers, timestamps, the five-minute window, the forged document — belongs to that one login.
The plain version#
A sealed letter of introduction#
Imagine a private members’ club that admits guests, but only guests introduced by a member organization. You do not have a membership card. What you have is a letter.
You walk to the office of your employer. The person at the desk knows you: they have seen your face for six years, they know your staff number, they know which floor you sit on. You ask for a letter of introduction to the club. The clerk writes one out. It says who you are, which club it is addressed to, when it was written, that it stops being good in five minutes, and one or two extra facts the club has asked for — that you are allowed to approve expenses, say, and that you belong to the United Kingdom travel group.
Then the clerk does the important part. They fold the letter, drip wax on the fold, and press the company seal into it. Anyone in the world can look at the wax and see the company crest. Nobody outside the company can make that crest, because the seal itself is locked in a drawer that only the clerk can open.
You carry the letter to the club. The doorkeeper does not know you. The doorkeeper does not phone your employer. The doorkeeper looks at the wax, recognizes the crest, reads the letter, and lets you in. That is all single sign-on is. The club never learns your password because there is nothing about your password in the letter. The club only has to trust one thing: the crest.
There are three parties, and it is worth fixing them firmly.
The employer’s office is the thing that knows who you are and writes letters. In SAML’s words it is the identity provider. Northwind’s office is at login.northwind.example.
The club is the thing you actually want to use. It holds the resource, it has a door, it accepts letters. In SAML’s words it is the service provider. Voyagr is the club.
You are the person the letter is about, walking between the two carrying the paper. In SAML’s words you are the subject, and the browser you are using is called the user agent because it acts for you.
The letter itself is called an assertion, and it is the heart of the whole system. Everything else in SAML is arrangements about how the letter gets written, how it travels, and what the doorkeeper checks.
What has to be written on the letter#
A letter that only said “this is Ravi” would be useless. Think about the ways a bare name could be abused, and you will rediscover, one by one, every field SAML actually requires.
If the letter has no date on it, then a letter written three years ago is as good as one written this morning. So the letter carries the moment it was written, and a moment after which it is worthless. In Ravi’s case the letter is written at nine fourteen in the morning and dies at nine nineteen. Five minutes. That is long enough for a browser to walk it across a network and short enough that a stolen letter is usually a stolen dead letter.
If the letter does not say which club it is for, then a letter you obtained honestly at one club can be carried across the street and used at another. So the letter names its intended recipient, explicitly, and a well-run doorkeeper refuses any letter not addressed to their own club.
If the letter does not say how the clerk checked you, then the club cannot tell the difference between “we watched him type a password” and “we recognized the laptop”. So the letter records the method of checking, which SAML calls the authentication context.
If the letter does not say when the clerk last actually looked at you, the club cannot tell whether you were verified this morning or last March. So the letter carries the moment of checking, separately from the moment of writing.
And if the letter carries no facts beyond the name, then the club has to keep its own file on you. So the letter can carry attributes: your email address, your groups, your cost centre, your job title. That is how a brand new user can appear inside an application already in the right team, without anybody having created an account first.
The three ways the letter can travel#
Here the design gets more interesting than a paper analogy usually allows, because there are genuinely three different ways to move a letter, and they are not equivalent.
The first way is you carry it in your hand, folded small. It is quick, it needs no arrangement in advance, and there is a limit to how much you can fold into a small piece of paper before it stops fitting in your fist. Anyone standing behind you in the queue can see the folded letter, and the doorman’s ledger will record what you were carrying. This is the HTTP-Redirect binding. The message is squeezed down, put into the web address itself, and the browser carries it.
The second way is you carry it in a sealed envelope handed over at the desk. There is no size limit worth worrying about. It is not written into the address, so it does not appear in the doorman’s ledger or in the browser’s history. This is the HTTP-POST binding, and it is what almost everybody uses for the letter coming back from the office, because that letter is large.
The third way is you carry a cloakroom ticket, not the letter at all. The office keeps the letter. It gives you a small numbered ticket, forty-four characters of nothing in particular. You hand the ticket to the club. The club then telephones the office directly, quotes the number, and the office reads the letter down the phone. Nothing sensitive ever travels through your hands. This is the Artifact binding, and it is the most private of the three and the most operationally awkward, because it requires the club and the office to be able to telephone each other directly, which across a firewall is not always true.
Two ways the trip can start#
There are two ways Ravi’s morning can begin, and the difference between them is larger than it looks.
In the first, Ravi goes to Voyagr first. Voyagr sees that he has no session, and says, in effect, “go and get a letter, and here is a note explaining that I asked for one”. Ravi is bounced to Northwind’s office, gets the letter, and comes back. When he arrives, Voyagr can match the letter against the note it wrote. This is called service-provider-initiated login, and it is the safer of the two, because Voyagr knows the letter is an answer to a question it actually asked.
In the second, Ravi starts inside Northwind’s own staff portal, sees a tile labelled “Voyagr”, clicks it, and the office writes a letter unprompted and sends him off with it. Voyagr receives a letter it never asked for. This is called identity-provider-initiated login, and it is enormously popular because it makes a pleasant portal page full of tiles. It is also, structurally, a letter arriving out of nowhere. Voyagr has no note to compare it against. Everything that would have been caught by “did I ask for this” has to be caught some other way, or not at all.
Ravi’s morning, with real values#
Let us do it once, all the way through, with the numbers written down.
At 09:14:19 on 17 August 2026, Ravi types his password into Northwind’s sign-in page and completes his second factor. Northwind’s office notes the time of that check.
Three seconds later, at 09:14:22, the office writes the letter. It gives the letter a unique reference: _a91c4f0e5d2b7a3418c96f0d5e1b2a7c. It writes on the letter that it is good from 09:14:22 until 09:19:22 — a five-minute life. It writes that the letter is for Voyagr and nobody else. It writes Ravi’s identifier, his email address, and two group memberships. It records that the check was done with a password over a protected connection. It seals the letter.
The letter goes into a sealed envelope — an ordinary web form that submits itself — and Ravi’s browser delivers it to a single specific address at Voyagr: voyagr.example/saml/acs. That address exists for exactly one purpose, which is to receive letters. It is called the assertion consumer service.
Voyagr’s doorkeeper now performs a fixed list of checks, in order. Is the wax crest one we recognize as Northwind’s. Is the seal intact over the whole letter. Is the letter addressed to us. Is the current time inside the five-minute window. Have we seen this exact letter reference before. Was it an answer to a note we wrote. If all six answers are right, Voyagr creates its own ordinary session cookie for Ravi and forgets the letter entirely, except for the reference number, which it remembers just long enough to reject a duplicate.
The whole thing takes about four hundred milliseconds and Ravi sees an expense form.
Why the seal is the only thing that matters#
Notice what the club never does. It never checks Ravi’s password, because it does not have one. It never phones the office, in the common case. It never looks Ravi up in a database. It makes one judgement — is this crest genuine and unbroken — and everything else follows.
That is the great strength of the design and the source of every weakness in this chapter. The entire trust of the system rests on the seal, and on the doorkeeper reading the letter that the seal was pressed onto. If somebody can produce a genuine-looking crest, they can be anyone. If somebody can arrange for the seal to be checked against one letter while the doorkeeper reads a different letter, they can be anyone. Everything that follows is a variation on that second sentence.
Where the plain version stops being true#
A letter is a flat page; an XML document is a tree#
The analogy said “letter”. A letter is a flat thing. You read it from the top, the words are in an order, and there is only one of everything.
An XML document is not like that. It is a tree of nested elements, and it can happily contain three things called Assertion in three different places, at three different depths. It can contain an element whose name you have never heard of, sitting between two you have. It can carry namespace declarations, which are labels attached to names that change what a name means, inherited downward through the tree and overridable at any point. It can carry comments, which are pieces of text that are supposed to be invisible. It can carry processing instructions and entity declarations, which are instructions to whatever program is reading it.
The honest version: SAML does not move a document from one program to another. It moves a sequence of bytes, which each program turns into its own tree, and the two trees need not be the same tree. Almost every attack in the second half of this chapter is a way of making the signature-checking code and the fact-reading code build different trees, or walk the same tree differently.
The seal does not cover the envelope#
In the analogy, one seal covered one letter, and it was obvious what was inside the wax and what was outside.
In SAML the outer envelope is itself an XML document — a Response — and the letter is an Assertion nested inside it. There are therefore two entirely different things that can be signed: the Response, or the Assertion, or both, or, in some deployments, neither, because somebody thought transport encryption was enough.
This matters enormously and it is the single most common source of confusion in real deployments. If only the Response is signed, then the assertion inside it is protected only for as long as it stays inside that response. Pull it out, put it in a different response, and its own integrity protection is nil, because it never had any. If only the Assertion is signed, the assertion travels safely but the wrapper around it — including, on some paths, the status code that says whether authentication succeeded — is unprotected.
The honest version: “the SAML response was signed” is not a security property until you say which element the signature reference points at. Ask that question of every integration you are given. The SAML V2.0 Profiles specification settles the minimum in section 4.1.4.5: when the HTTP-POST binding is used to deliver the response, the enclosed assertion or assertions must be signed. Signing the response as well is fine, and is common, but the assertion signature is the one that must exist.
“The signature is valid” is a claim about bytes, not about meaning#
In the analogy the doorkeeper looks at the wax and then reads the letter under it, and there is no gap between those two acts because they are physically the same piece of paper.
In software they are two separate function calls, often in two separate libraries, minutes apart in the code. The first call answers: is there some element in this document whose digest matches a signed reference, using a key I trust. The second call answers: what is the user’s name. Nothing in the design forces those two calls to be talking about the same element.
The honest version: an XML signature does not sign a document. It signs a set of references, each pointing at a node by identifier, each with a digest of that node after transformation. A document can contain a perfectly valid signature over a node that the application never looks at. The 2012 USENIX Security paper by Somorovsky and colleagues is titled “On Breaking SAML: Be Whoever You Want to Be” precisely because this gap is exploitable in practice, not in theory. They examined fourteen SAML frameworks and found eleven of them vulnerable, including implementations from Salesforce, Shibboleth and IBM.
Invisible things are not invisible to everybody#
The analogy has no equivalent of an XML comment, and that is the problem, because a comment is a piece of text that is defined to have no meaning and which some programs nevertheless read.
Before a signature is computed, the document is put through a normalizing step called canonicalization, which rewrites it into one fixed byte-for-byte form so that harmless differences — attribute order, whitespace inside a tag, which of several equivalent ways a namespace was declared — do not change the answer. The version SAML uses discards comments. That is deliberate and correct: a comment carries no meaning, so it should not change a signature.
But some programming interfaces for reading text out of an XML element return only the text up to the first comment. So the string that gets signed and the string that gets read can be different strings, using nothing but a construct that is meant to be inert.
The honest version: canonicalization deliberately erases comments so they cannot break a signature, and that same erasure is what lets a comment change what a careless reader sees while leaving the signature intact. Duo Labs published this in February 2018 and it affected at least six independent libraries at once.
The club is supposed to check the address, and often does not#
The analogy said the letter names its club. It does. The Assertion contains an AudienceRestriction condition naming the intended service provider.
In practice, a large number of service providers do not check it, or check it against a value that is configurable per tenant and never reviewed. The consequence is direct: any letter you can obtain honestly at any service provider trusting the same identity provider can be replayed at any other one that does not check.
The honest version: the audience check is not optional and it is not a formality. It is the only thing standing between “I have a valid assertion” and “I have a valid assertion for you”.
The letter that nobody asked for#
Identity-provider-initiated login is the friendly one with the tiles, and it removes a security control rather than adding a feature.
In service-provider-initiated login, the service provider generates an identifier for its request, and the assertion that comes back carries that identifier in an InResponseTo attribute. Matching them proves that this assertion is an answer to this request, in this browser, in this tab, right now. In identity-provider-initiated login there is no request, so there is no identifier, so there is nothing to match. The SAML Profiles specification calls these unsolicited responses and handles them in section 4.1.5.
The honest version: identity-provider-initiated SSO is a login flow with the freshness and correlation check deliberately removed. It can be operated safely, with tight time windows, strict single-use tracking of assertion identifiers, and audience checks that are actually enforced, but it is strictly weaker and should be turned off wherever a portal tile is not a hard requirement.
The seal proves who wrote it, not that it is still true#
A signature proves origin. It says nothing whatever about whether the statement remains true, or whether this is the fourth time you have presented the same letter.
The honest version: SAML has no revocation for assertions. There is no list to check. The only defences against a replayed assertion are the NotOnOrAfter time and the service provider remembering identifiers it has already accepted. If your service provider accepts an eight-hour assertion validity window because somebody complained about clock skew, you have an eight-hour replay window.
And the seal can be stolen#
Every analogy about wax ends the same way. If somebody takes the seal out of the drawer, they can write any letter they like about anybody, and every doorkeeper in the world will accept it.
The identity provider’s private signing key is the crown jewel of an entire organization’s access. In November 2017 CyberArk’s Shaked Reiner published a technique he named Golden SAML: extract the token-signing key from a federation server and forge assertions for any user, at any service provider, with no interaction with the identity provider at all. It moved from research to headline in December 2020, when investigators of the SolarWinds intrusion found forged SAML tokens used exactly this way.
The honest version: SAML’s security model reduces, in the end, to the physical and operational protection of one private key. Everything else in this chapter is about the paths that let you skip needing it.
The technical version#
The four layers, and which document defines each#
SAML is not one specification. It is a set of them, approved together as an OASIS Standard in March 2005, with the specification documents themselves dated 15 March 2005. The core document alone runs to 86 pages, and it is the smallest interesting part of the family. Understanding SAML means understanding that it is built in four layers, and that a sentence like “we use SAML” specifies almost nothing until you say which choice was made at each layer.
The assertion layer defines the statement itself: what an identity provider can say about a subject, and in what shape. This is the Assertion element and everything under it. It is defined in the Assertions and Protocols document, usually called Core.
The protocol layer defines the request and response pairs that cause assertions to be produced or acted on: the authentication request protocol, the artifact resolution protocol, the single logout protocol, the name identifier management protocol. These are also in Core.
The binding layer defines how a protocol message is carried by an actual transport: in a URL, in a form field, in a SOAP envelope. This is the Bindings document.
The profile layer defines a complete, named combination of the other three that solves a real problem: which messages, in which order, over which bindings, with which checks. This is the Profiles document. The profile you care about is Web Browser SSO.
| Layer | Defines | Document |
|---|---|---|
| Assertions | Statements about a subject | Core |
| Protocols | Request and response pairs | Core |
| Bindings | Message on a transport | Bindings |
| Profiles | Named end-to-end recipes | Profiles |
Four more documents complete the standard: Metadata, which describes how two parties publish their configuration to each other; Authentication Context, which describes how strongly a subject was checked; Conformance, which says what an implementation must do to claim support; and Security and Privacy Considerations, which is the one nobody reads and the one that would have prevented several of the incidents later in this chapter.
One more document is worth knowing about. The approved errata to SAML 2.0 were posted on 1 May 2012, following a public review draft of 21 February 2012, and contain eighty-odd corrections. Item E83, titled “Weaken claim made about Exclusive C14N”, downgrades a requirement in the Security and Privacy Considerations document from a mandate to a recommendation. That single erratum is a miniature of the whole subject: one normative keyword changed, in a document about canonicalization, with consequences that took another six years to appear in the field.
An assertion, annotated#
Here is Ravi’s assertion, with the algorithm identifiers shortened so the lines fit on a printed page. Everything else is real: real element names, real attribute names, real formats.
<saml:Assertion Version="2.0"
ID="_a91c4f0e5d2b7a3418c96f0d5e1b2a7c"
IssueInstant="2026-08-17T09:14:22Z">
<saml:Issuer>https://login.northwind.example/idp</saml:Issuer>
<ds:Signature>... shown separately below ...</ds:Signature>
<saml:Subject>
<saml:NameID
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
f2c81b9a-0e44-4d31-9f77-6b2d0a1c88e5
</saml:NameID>
<saml:SubjectConfirmation
Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
NotOnOrAfter="2026-08-17T09:19:22Z"
Recipient="https://voyagr.example/saml/acs"
InResponseTo="_req5f1c9d2a7b3e4086"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions
NotBefore="2026-08-17T09:14:22Z"
NotOnOrAfter="2026-08-17T09:19:22Z">
<saml:AudienceRestriction>
<saml:Audience>
https://voyagr.example/saml/metadata
</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement
AuthnInstant="2026-08-17T09:14:19Z"
SessionIndex="_s3d8e1f0b6c47"
SessionNotOnOrAfter="2026-08-17T17:14:19Z">
<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="mail">
<saml:AttributeValue>
ravi.menon@northwind.example
</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="groups">
<saml:AttributeValue>expenses-approvers</saml:AttributeValue>
<saml:AttributeValue>uk-travel</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
Read it as five separate claims, because that is what it is.
The outer attributes are administrative. Version is always the literal string 2.0 in SAML 2.0; the older MajorVersion and MinorVersion pair from SAML 1.1 was collapsed into one attribute. ID is the reference that a signature points at and that a service provider remembers in order to reject a replay; it is required to be unique and to be unguessable in practice, though the specification’s requirement is on uniqueness rather than randomness. IssueInstant is when the assertion was written, in UTC, in the XML Schema dateTime format.
The Issuer is the identity provider’s entity identifier. It looks like a web address and usually is one, but it is an identifier, not necessarily a location. Two identity providers with the same certificate and different entity identifiers are two different parties as far as SAML is concerned.
The Subject says who the assertion is about, and separately, how the recipient may confirm that the party presenting the assertion is that subject. Those are two different things and the specification defines them in sections 2.4.1 and 2.4.1.1 of Core respectively. The NameID is the identifier. Its Format matters: the persistent format shown here is an opaque, per-service-provider pseudonym designed so that two service providers cannot correlate the same user; the transient format is a throwaway that changes every login; the emailAddress format is a real address and is the most commonly used and the least privacy-preserving. The SubjectConfirmation names the method. In web SSO the method is nearly always bearer, which means precisely what it says: whoever holds this assertion is treated as the subject. There is no key involved, no proof of possession, nothing but possession itself.
The Conditions element carries the validity window and the audience. NotBefore and NotOnOrAfter bracket the time. AudienceRestriction names who the assertion is for, using the service provider’s entity identifier — not its hostname, not its assertion consumer service address, its entity identifier. Core section 2.5.1.5 also defines a OneTimeUse condition, which asks the recipient not to cache the assertion; it is rarely used and even more rarely enforced.
The AuthnStatement is the actual authentication event. AuthnInstant is when the identity provider verified the subject — 09:14:19, three seconds before the assertion was written. SessionIndex is the identity provider’s handle for this login session, and it exists almost entirely so that single logout can later say “end that one”. SessionNotOnOrAfter is the identity provider’s opinion about how long its own session should last, which a service provider may honour or ignore. The AuthnContextClassRef is a URI naming how the check was done; the value here means a password over a protected transport.
The AttributeStatement carries the facts. Note that a single Attribute can have several AttributeValue children, which is how group membership is expressed. This is a genuine advantage of SAML that is often lost in comparisons: multi-valued attributes are native, ordinary and understood by every implementation, with no encoding convention needed.
Now the signature that was elided, shown separately, with algorithm identifiers abbreviated:
<ds:Signature>
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm=".../xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="...xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_a91c4f0e5d2b7a3418c96f0d5e1b2a7c">
<ds:Transforms>
<ds:Transform Algorithm="...#enveloped-signature"/>
<ds:Transform Algorithm=".../xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="...xmlenc#sha256"/>
<ds:DigestValue>Zk9rT2h4RGx1WjBhNnFwMQ==</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
Qm5xY0FoZjJyVHhLbTBwOFdzTmQ3VmUzeUxiSjRnUXo=
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIDvzCCAqegAwIBAgIUZ1p4ck0yc3RRd0V4 ... truncated
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
Four things in that block do all the work. The Reference URI is a fragment identifier pointing at the ID attribute of the element being signed. The Transforms say what to do to that element before hashing: strip out the signature itself, since a signature cannot contain its own digest, then canonicalize. The DigestValue is the hash of the transformed element. The SignatureValue is the signature over the SignedInfo block, not over the document. Everything the signature guarantees is inside SignedInfo, and SignedInfo is a set of pointers.
Core section 5.4.2 requires a reference to a signed assertion to use the fragment identifier mechanism against the assertion’s ID attribute. Section 5.4.3, Canonicalization Method, directs implementations to exclusive canonicalization so that a signature made over an assertion embedded in one XML context can still be verified in another. Section 5.4.4, Transforms, restricts the permitted transforms; the enveloped signature transform plus exclusive canonicalization is the expected pair, and anything else in a production assertion is a defect or an attack.
Conditions, confirmation and the replay window#
The five-minute window in Ravi’s assertion is a convention, not a standard. Nothing in SAML 2.0 sets a number. What the specification does is require the checks, and the numbers are yours.
There are, confusingly, two NotOnOrAfter attributes in a typical assertion and they mean different things. The one on Conditions bounds the validity of the assertion as a statement. The one on SubjectConfirmationData bounds how long the bearer confirmation may be used to establish the subject. They are usually set to the same value and they are not the same claim. The Profiles specification, section 4.1.4.3, is the one that tells a service provider to check the SubjectConfirmationData: that its Recipient matches the assertion consumer service address the message actually arrived at, that its NotOnOrAfter has not passed, and that its InResponseTo equals the identifier of the request the service provider sent.
Clock skew is the practical enemy. Two machines whose clocks differ by ninety seconds will reject perfectly good assertions if the window is tight and no tolerance is allowed. The engineering answer is a small explicit tolerance, typically thirty to sixty seconds, plus time synchronization on both ends. The common bad answer is to widen the assertion lifetime to hours, which converts a clock problem into a replay problem.
| Check | Where defined | Typical value |
|---|---|---|
| Assertion lifetime | Conditions | 5 minutes |
| Bearer window | ConfirmationData | 5 minutes |
| Clock tolerance | Local policy | 30 to 60 s |
| Replay cache | Local policy | Lifetime plus skew |
The replay cache is the check people skip. A bearer assertion that passes every other test can be presented twice. The only defence is for the service provider to store the assertion ID for at least as long as the assertion could remain valid, and reject a second presentation. Profiles section 4.1.4.5 states the requirement plainly: the service provider must ensure that bearer assertions are not replayed, by maintaining the set of used ID values for as long as the assertion would be considered valid. In a load-balanced deployment that means a shared cache, not a per-process dictionary, and that is where the requirement quietly dies in a great many real systems.
The protocol layer: requests and responses#
The authentication request protocol is small. The service provider sends an AuthnRequest. The identity provider sends back a Response containing zero or more assertions and a status code.
Ravi’s request, decoded from what the browser actually carried:
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="_req5f1c9d2a7b3e4086"
Version="2.0"
IssueInstant="2026-08-17T09:13:58Z"
Destination="https://login.northwind.example/idp/sso"
AssertionConsumerServiceURL=
"https://voyagr.example/saml/acs"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:
bindings:HTTP-POST">
<saml:Issuer>https://voyagr.example/saml/metadata</saml:Issuer>
<samlp:NameIDPolicy AllowCreate="true"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/>
</samlp:AuthnRequest>
The ID is the value that will come back as InResponseTo. The Destination is the address the request is being sent to, and it exists so that a request captured on one endpoint cannot be replayed at another. The AssertionConsumerServiceURL asks for the response to be sent to a particular address, and this attribute is a well-known hazard: an identity provider that honours a requested address without checking it against the registered metadata for that service provider has handed an attacker a way to redirect assertions. The correct behaviour is to accept the attribute only if it exactly matches a registered endpoint, and many identity providers offer a configuration switch to require an index into the registered list instead of a free-text address.
There are two more attributes worth knowing. ForceAuthn, when true, tells the identity provider to re-check the subject even if a session exists, which is what a step-up for a sensitive action needs. IsPassive, when true, tells the identity provider not to interact with the user at all, which is what a silent session check needs; if interaction would be required, the identity provider returns a status of NoPassive rather than showing a page.
The response wrapper is equally small, and its Status element is the part people forget to protect:
<samlp:Response ID="_r7714b0e3c1a95d2f"
InResponseTo="_req5f1c9d2a7b3e4086"
Destination="https://voyagr.example/saml/acs"
IssueInstant="2026-08-17T09:14:22Z" Version="2.0">
<saml:Issuer>https://login.northwind.example/idp</saml:Issuer>
<samlp:Status>
<samlp:StatusCode
Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion> ... as shown above ... </saml:Assertion>
</samlp:Response>
The three bindings compared#
The three bindings you will meet in browser SSO differ on four axes: where the message sits, how big it may be, who can see it, and whether a direct connection between the two servers is needed.
HTTP-Redirect, Bindings section 3.4, puts the message in the query string of a redirect. The encoding is specific and worth knowing because it is the source of a standard class of bug. The XML is compressed with raw DEFLATE as defined in RFC 1951 — raw, with no zlib header and no checksum, which is why a library that produces zlib-wrapped output produces messages nobody can read. The compressed bytes are base64 encoded, then URL-encoded, then placed in a parameter named SAMLRequest or SAMLResponse. RelayState, if present, is a separate parameter and is opaque to the protocol; it is how a service provider remembers which page the user was trying to reach.
Signing works differently on this binding, and this is the detail that catches people. There is no XML signature. Any signature that was inside the XML must be removed before encoding. Instead, the signature is computed over an octet string built from the URL-encoded query parameters concatenated in a fixed order — SAMLRequest or SAMLResponse first, then RelayState if present, then SigAlg — with the algorithm named in a SigAlg parameter and the result in a Signature parameter. Bindings section 3.4.4.1 defines that construction exactly.
GET /idp/sso?
SAMLRequest=fVLLTsMwEPwVy3fHTloSaqWRCggJCS5Q ...
&RelayState=%2Fexpenses%2F2026-08
&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2F ...
&Signature=hK9mQ2%2BvT0pRc3lXbjRkR2ZQdw%3D%3D ...
Host: login.northwind.example
The consequence is that a verifier must check the raw query string as received, not a re-serialization of the parsed parameters. Re-encoding or reordering changes the bytes and breaks the check.
HTTP-POST, Bindings section 3.5, base64 encodes the XML without compression and places it in a hidden form control named SAMLRequest or SAMLResponse, in a form that submits itself with a small piece of script or a submit button. RelayState travels in a control of the same name. The signature is a real XML signature inside the document. There is no practical size limit. This is the binding used for the response in almost every deployment, because a response with a signed, attribute-laden assertion is far too large to fold into a URL.
HTTP-Artifact, Bindings section 3.6, sends a reference instead of a message. The artifact for SAML 2.0 has type code 0x0004 and is exactly 44 bytes before encoding: a two-byte type code, a two-byte endpoint index, a twenty-byte SourceID and a twenty-byte MessageHandle. Bindings section 3.6.4.2 specifies that the issuer constructs the SourceID by taking the SHA-1 hash of its identification URL, and that the MessageHandle comes from a cryptographically strong random source. Base64 encoded, those 44 bytes become 60 characters, which fits comfortably in a URL. The receiver then makes a direct back-channel call — an ArtifactResolve request over the SOAP binding — and gets the real message back in an ArtifactResponse.
| Binding | Message location | Needs back channel |
|---|---|---|
| HTTP-Redirect | URL query string | No |
| HTTP-POST | Self-posting form | No |
| HTTP-Artifact | 44-byte reference | Yes |
The trade is straightforward. Redirect leaks the message into browser history, proxy logs and Referer headers, and is size-limited by whatever the intervening infrastructure tolerates. POST leaks nothing into the URL but puts the full assertion through the browser, where a malicious extension or a compromised page can read it. Artifact puts nothing sensitive through the browser at all, which is why it remains popular in higher-security government and health deployments, and requires the service provider to be able to reach the identity provider directly, which is why it is unpopular everywhere else. There is a fourth binding, Reverse SOAP or PAOS, in section 3.3, used by the Enhanced Client or Proxy profile for clients that are not browsers.
The Web Browser SSO profile, drawn out#
Profiles section 4.1 defines the profile that carries the overwhelming majority of real SAML traffic. Here is the service-provider-initiated form, which is the one you should prefer.
Browser Voyagr (SP) Northwind (IdP)
| | |
1 | GET /expenses | |
|----------------->| |
2 | 302 with AuthnRequest in query |
|<-----------------| |
3 | GET /idp/sso?SAMLRequest=... |
|--------------------------------------->|
4 | password, second factor, consent |
|<-------------------------------------->|
5 | 200 HTML form, SAMLResponse hidden |
|<---------------------------------------|
6 | POST /saml/acs | |
|----------------->| |
7 | 302 /expenses + session cookie |
|<-----------------| |
Steps 2 and 3 use the HTTP-Redirect binding; steps 5 and 6 use HTTP-POST. That asymmetry is the standard arrangement, and it exists for the size reason given above: the request is small, the response is not.
The identity-provider-initiated form removes steps 1, 2 and 3 entirely. The user is already at the identity provider, clicks a link, and the identity provider generates a response with no InResponseTo attribute and posts it to the service provider’s assertion consumer service. Profiles section 4.1.5 calls this an unsolicited response and states that the response must not contain an InResponseTo attribute, which is the honest way of saying that the correlation check is unavailable.
If you must support it, three compensations are non-negotiable. Keep the assertion window very short, because the window is now the entire attack surface. Enforce the replay cache strictly, because a captured assertion is a complete credential. Enforce the audience restriction absolutely, because there is nothing else tying this assertion to your application. A fourth is worth adding as policy: do not allow an unsolicited response to land the user anywhere except a fixed landing page, because the RelayState on an unsolicited response is attacker-controllable and has been used as an open redirect more than once.
Where the signature sits, and why it decides everything#
There are four possible arrangements, and they are not equally safe.
| Signed element | Assertion protected | Status protected |
|---|---|---|
| Assertion only | Yes | No |
| Response only | Only in place | Yes |
| Both | Yes | Yes |
| Neither | No | No |
Signing the assertion only is the arrangement the Profiles specification requires as a minimum for the POST binding, and it is what most identity providers do by default. The assertion is self-protecting: it can be pulled out of its response, put in another, forwarded, stored, and its integrity claim travels with it.
Signing the response only is the dangerous one, and real products offer it as a configuration option. The assertion inside has no signature of its own, so a service provider whose code asks “is there a valid signature somewhere in this document” and then reads the first assertion it finds is one wrapping attack away from total compromise. A service provider that accepts either arrangement cannot enforce a simple rule, and complexity in the enforcement path is the ground on which these attacks are won.
Signing both is common and harmless, and doubles the verification work. Signing neither happens in internal deployments where somebody reasoned that the message travels over TLS between two servers they control; it does not, it travels through a browser, which is not part of your trust boundary.
The rule to write into your configuration is short. Require a signature over the assertion, and reject any response whose assertion is unsigned whatever else is signed. Reject any response containing more than one assertion. And when the assertion is encrypted, verify the signature over the decrypted assertion, not over some outer element.
XML signature wrapping, with a worked malicious document#
This is the attack the 2012 USENIX Security paper is about, and it is worth building by hand once because the mechanism is not intuitive until you have seen it.
Start from what a signature actually says. The SignedInfo block contains a Reference whose URI is #_a91c4f0e5d2b7a3418c96f0d5e1b2a7c. That is an instruction: find the element in this document whose ID attribute has that value, apply these transforms to it, hash it, and compare with the DigestValue. Note carefully what it does not say. It does not say where in the document that element is. It does not say it must be the first assertion, or a direct child of the response, or the only one. The reference is a name, and names float.
Now the attacker’s move. Ravi is honest; the attacker is a different Northwind employee, or anyone who has obtained one valid assertion by any means — from their own legitimate login at the same identity provider, from a log file, from a referrer header on a Redirect-bound deployment. They take that valid, correctly signed assertion and they do not modify it at all. They relocate it.
<samlp:Response ID="_r7714b0e3c1a95d2f">
<saml:Assertion ID="_evil0001">
<saml:Issuer>https://login.northwind.example/idp</saml:Issuer>
<saml:Subject>
<saml:NameID>finance.admin@northwind.example</saml:NameID>
</saml:Subject>
<saml:Conditions NotOnOrAfter="2026-08-17T23:59:00Z">
<saml:AudienceRestriction>
<saml:Audience>
https://voyagr.example/saml/metadata
</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AttributeStatement>
<saml:Attribute Name="groups">
<saml:AttributeValue>global-admins</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
<samlp:Extensions>
<saml:Assertion ID="_a91c4f0e5d2b7a3418c96f0d5e1b2a7c">
<saml:Subject>
<saml:NameID>ravi.menon@northwind.example</saml:NameID>
</saml:Subject>
<ds:Signature>
<ds:SignedInfo>
<ds:Reference
URI="#_a91c4f0e5d2b7a3418c96f0d5e1b2a7c">
<ds:DigestValue>Zk9rT2h4RGx1WjBhNnFwMQ==</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
Qm5xY0FoZjJyVHhLbTBwOFdzTmQ3VmUzeUxiSjRnUXo=
</ds:SignatureValue>
</ds:Signature>
</saml:Assertion>
</samlp:Extensions>
</samlp:Response>
The shape of the trick, drawn as a tree:
Response
|
+-- Assertion ID=_evil0001 <- application reads
| Subject: finance.admin@northwind.example
| groups: global-admins
| (no signature at all)
|
+-- Extensions
|
+-- Assertion ID=_a91c4f0e... <- verifier checks
Subject: ravi.menon@northwind.example
Signature Reference URI="#_a91c4f0e..."
digest still matches perfectly
Trace both code paths and the attack is obvious.
The verifier finds the Signature element. It reads the Reference URI. It resolves that identifier across the whole document and finds the original assertion, unmodified, sitting in the Extensions element. It applies the transforms, hashes, compares to the DigestValue — they match, because nothing about that element changed. It checks the signature over SignedInfo with Northwind’s public key — valid, because Northwind really did sign it. The verifier returns true. It is not wrong. It answered the question it was asked.
The application then calls something with a name like getAssertion() or response.assertions[0] or an XPath expression selecting the first Assertion child of Response. It gets the forged one. It reads finance.admin@northwind.example and a membership of global-admins. Ravi’s colleague is now a global administrator of the expense system.
The paper’s authors catalogued many variants. The wrapper can be an Extensions element, an Object element inside the signature itself, a bogus element in a namespace the schema validator ignores, or a second Response nested in the first. Where the schema is enforced strictly, attackers move the original into the places the schema permits arbitrary content, which is exactly what Extensions is for. Of the fourteen frameworks examined, eleven were vulnerable.
The fix is not a better signature algorithm. The fix is structural, and it has three parts.
First, extract from the verified object, never from the original document. The output of signature verification should be the specific element that was verified, and every subsequent read should go to that element and nothing else. Libraries that return a boolean and leave you holding the original document are handing you the bug.
Second, fix the position before verifying. Do not resolve the reference by searching the document for a matching identifier. Decide where the assertion is allowed to be — a direct child of Response, or the sole child of an EncryptedAssertion — select it by position, and require that the signature’s reference points at exactly that element.
Third, reject documents with more than one assertion, or with unexpected elements in the response. Structural strictness costs nothing and removes the whole class.
A fourth defence is often proposed and is not sufficient alone: schema validation. It helps, and it is defeated by extension points and by the fact that the parser used for validation is frequently not the parser used for extraction, which is the subject of the next section.
Comments, canonicalization and parser differentials#
Canonicalization is the step that makes XML signatures possible at all, and it is where the next two attack classes live.
The problem it solves is real. Two XML documents can be different byte sequences and mean exactly the same thing: attributes in a different order, a namespace declared on the parent instead of the child, whitespace inside a tag, an equivalent character encoding. A hash over raw bytes would break on all of these, and XML routinely passes through software that rewrites it harmlessly. So before hashing, the element is rewritten into one canonical byte form.
Canonical XML 1.0 became a W3C Recommendation in March 2001. Exclusive XML Canonicalization Version 1.0 followed on 18 July 2002, and it is the one SAML directs you to. The difference matters: exclusive canonicalization does not drag in namespace declarations from ancestor elements the element does not visibly use. That is precisely what allows an assertion signed inside one response to be verified after being moved into another — the same property that signature wrapping exploits. The security property and the vulnerability are two faces of one design decision.
Both forms come in two flavours, with comments and without. SAML uses without. A comment carries no meaning, so removing it before hashing means it cannot break a signature.
In February 2018, Kelby Ludwig at Duo Labs published the consequence. Some XML programming interfaces, asked for the text inside an element, return only the text node up to the first comment. Canonicalization removes the comment; the text-reading interface stops at it. Two different answers from one element.
Work it on Ravi’s system. Suppose an attacker obtains an account at Northwind’s identity provider with an address of their choosing under a domain they control. They register:
finance.admin@northwind.example.attacker.example
They sign in honestly. Northwind writes a genuine, correctly signed assertion naming that address. The attacker intercepts their own assertion — it is in their own browser, so this requires nothing clever — and inserts an empty comment:
<saml:NameID>
finance.admin@northwind.example<!---->.attacker.example
</saml:NameID>
The signature still verifies. Canonicalization strips the comment, producing the identical byte sequence that was signed, so the digest is unchanged. Then Voyagr’s code asks for the text of the NameID element using a call that returns the first text node, and receives:
finance.admin@northwind.example
Voyagr logs the attacker in as the finance administrator. No cryptography was broken. The CERT Coordination Center published this as vulnerability note VU#475445 on 27 February 2018, with six CVE identifiers assigned at once: CVE-2017-11427 for OneLogin’s python-saml, CVE-2017-11428 for OneLogin’s ruby-saml, CVE-2017-11429 for Clever’s saml2-js, CVE-2017-11430 for OmniAuth-SAML, CVE-2018-0489 for the Shibboleth OpenSAML C++ library, and CVE-2018-5387 for Wizkunde SAMLBase. Six independent teams made the same mistake because the mistake is in the shape of the interface, not in any one team’s judgement.
The bug did not go away; it changed clothes. In March 2025 two research efforts landed within days of each other, both exploiting the fact that a SAML library uses more than one XML parser.
GitHub Security Lab found that ruby-saml verified signatures using two parsers: REXML, the pure-Ruby parser, and Nokogiri, a wrapper over libxml2. The same document given to both could be made to yield different signature elements for the same query, so the SignedInfo block that was canonicalized and checked came from one parser’s view while the SignatureValue it was checked against came from the other’s. An extra Signature hidden inside a StatusDetail element was visible to one parser and not the other. The result was a complete authentication bypass, assigned CVE-2025-25291 and CVE-2025-25292, fixed in ruby-saml 1.18.0 and backported to 1.12.4, with a follow-up advisory for an incomplete namespace-handling fix.
In the same month, WorkOS published findings from the researcher known as ahacker1 under the name SAMLStorm, affecting the Node.js library xml-crypto at version 6.0.0 and earlier as CVE-2025-29774 and CVE-2025-29775, fixed in 6.0.1 and backported to 3.2.1 and 2.1.6. The core is the same comment trick, moved down into the signature machinery itself:
<DigestValue><!-- forged_digest -->legitimate_digest</DigestValue>
The reader of the digest value stopped at the comment; the canonical form did not. The related node-saml advisory CVE-2025-54419 is the same family: assertion data read from the untrusted document rather than from the verified content.
The honest lesson across seven years and a dozen CVE identifiers is one sentence: never read a value from a document by a different route than the route the signature verified it by. Not a different parser, not a different traversal, not a different text-extraction call. One parse, one tree, one extraction path.
There is a second lesson for library authors and it is unpopular: an interface that returns a boolean from signature verification is an unsafe interface. The safe interface returns the verified content, and makes the unverified document inaccessible afterwards.
XML external entities and the pre-verification parse#
The third XML-level hazard has nothing to do with signatures. It is about what an XML parser will do before anyone has verified anything.
XML permits a document to declare entities — named substitutions — in a document type declaration, and permits those entities to point at external resources. A parser configured with the historical defaults will fetch them. That gives an attacker who can send a document to your parser two capabilities: read a local file into the document, and make your server issue a network request to an address of the attacker’s choosing.
<?xml version="1.0"?>
<!DOCTYPE Response [
<!ENTITY leak SYSTEM "file:///var/run/secrets/token">
]>
<samlp:Response>
<saml:Assertion>
<saml:Subject>
<saml:NameID>&leak;</saml:NameID>
</saml:Subject>
</saml:Assertion>
</samlp:Response>
Note the ordering that makes this bite. A SAML endpoint must parse the document before it can find the signature, and find the signature before it can verify it, so the parse of a wholly untrusted document always happens first. It does not matter that the signature will fail a moment later. By then the file has been read, and it will frequently appear in an error message or a log line, which is all the attacker needs.
This is not historical. CVE-2026-28809, published in March 2026 and updated in July 2026, describes exactly this in esaml, a SAML library for Erlang, and in its forks. The library parses attacker-controlled messages before signature verification without disabling entity expansion, and on Erlang/OTP releases before 27 the underlying XML parser allowed entities by default. The advisory’s own example of what an attacker would read is a Kubernetes-mounted secret. Deployments on OTP 27 and later are unaffected because the parser default changed.
A related denial-of-service form uses internal entities only, expanding a small document to gigabytes through nested references. It needs no external access, so blocking network fetches while permitting entity declarations does not stop it.
The defence is configuration, and it is the same settings in every ecosystem whatever the local names are. Disallow document type declarations outright, which removes entity declarations, entity expansion and external DTD loading in one move. Disable external general and parameter entities. Disable network access from the parser. Then apply a hard size limit before parsing, and an expansion limit if the parser offers one.
| Setting | Effect | Stops |
|---|---|---|
| Disallow doctype | No DTD at all | XXE and expansion |
| No external entities | No file or net fetch | File disclosure |
| No network from parser | Blocks callbacks | SSRF and blind XXE |
| Size limit before parse | Bounds the work | Expansion floods |
One more ordering rule belongs here. Do not log the parsed content of an unverified document. Log the fact of failure, the source address, and a truncated identifier. Every XXE finding of the last decade has had a variant where the data escaped through the error path rather than the success path.
Metadata, entity identifiers and certificate rollover#
None of the above works unless two organizations can tell each other what their addresses and keys are. SAML’s answer is metadata: a signed XML document describing an entity.
The unit is the EntityDescriptor, defined in Metadata section 2.3.2, and its entityID attribute is the entity’s name. Section 2.2.1 defines the type as a URI of at most 1024 characters, unique across all entities that interact in a given deployment. Two properties of it cause more confusion than anything else in SAML operations. It is an identifier, not an address: nothing has to be reachable there, and many production entity identifiers deliberately are not. And it is compared as an exact string: a trailing slash or a change of scheme makes it a different entity, and the resulting failure looks like a signature problem rather than a naming problem.
Inside the EntityDescriptor sit role descriptors: IDPSSODescriptor in section 2.4.3, SPSSODescriptor in section 2.4.4. Each carries endpoints — single sign-on addresses, assertion consumer service addresses with their bindings and index numbers, single logout addresses — and KeyDescriptor elements, defined in section 2.4.1.1, each optionally carrying a use attribute of either signing or encryption.
Two attributes govern freshness. validUntil is the expiration time of the metadata itself. cacheDuration is the maximum time a consumer should hold it. They are not the same and both are commonly ignored, which is how a federation ends up running on a metadata file somebody downloaded in 2019.
Now the surprising part, and it is the one thing about SAML metadata that experienced PKI engineers get wrong. The SAML V2.0 Metadata Interoperability Profile Version 1.0, an OASIS Committee Specification dated 4 August 2009, says that once a metadata instance has been accepted, a consumer must not apply additional criteria to the validity of the keys inside it — and it is explicit that this excludes X.509 path validation, certificate revocation lists and OCSP responders.
Read that again, because it inverts the usual instinct. The certificate in a KeyDescriptor is not being used as a certificate. It is a container for a public key. Its expiry date, its issuer and whether it is self-signed are all irrelevant. The trust comes from the metadata document, whose own signature and provenance are what you must protect. Deployments that reject expired certificates in metadata are not being extra careful; they are diverging from the interoperability profile, and they cause outages that look inexplicable.
That is also what makes certificate rollover work. Because trust is by key comparison, the procedure has no cryptographic complications; the whole difficulty is scheduling.
- Generate the new key pair and certificate at the identity provider well before the old one is needed, and do not use it yet.
- Publish metadata containing both KeyDescriptor elements with use of signing: the current one and the new one.
- Wait. The waiting period must exceed the longest cacheDuration anyone downstream is honouring, plus the slowest manual update in the federation. In a bilateral integration this may be days. In a large multilateral federation it may be weeks.
- Confirm that consumers have the new metadata. This is the step that is skipped, and the step that causes every rollover outage.
- Switch the identity provider to sign with the new key. Consumers that hold both keys accept the new signature immediately.
- Wait again, then publish metadata with the old KeyDescriptor removed.
The same sequence run backwards handles service provider encryption keys: publish the new encryption key, wait, then remove the old one, because the identity provider must know the new key before it can encrypt to it.
Getting metadata from one party to the other is the part SAML never fully standardized in 2005. Three approaches exist in practice. Manual exchange of a file, verified out of band by a fingerprint read over the telephone, is a convention rather than a standard and remains extremely common for a single bilateral integration. A signed aggregate — one large file containing every entity in a federation, signed by the federation operator and downloaded on a schedule — is what large research and education federations have done for years. And per-entity retrieval on demand is the Metadata Query Protocol, specified in the Internet-Drafts draft-young-md-query and its SAML profile draft-young-md-query-saml. As of August 2026 both remain Internet-Drafts rather than RFCs, with the SAML profile at revision 25 dated June 2026: a mechanism large federations depend on operationally has been a draft for over a decade. The federation agreements themselves — who trusts whom, and on what contractual basis — belong to chapter 43.
Golden SAML, and the key that must not leak#
Everything in this chapter has been about avoiding the need for the signing key. It is worth spending a paragraph on what happens when an attacker simply takes it.
In November 2017, Shaked Reiner of CyberArk published a technique named Golden SAML. If you can extract the token-signing private key from an organization’s federation server, you can mint valid assertions for any user, at any service provider that trusts that identity provider, from anywhere, without ever touching the identity provider again. There is no password to steal, no second factor to satisfy, and no login event in the identity provider’s logs, because there is no login. It stopped being theoretical in December 2020, when investigators of the SolarWinds supply-chain intrusion documented forged SAML tokens used this way against cloud services.
Three consequences follow. The token-signing key belongs in a hardware security module or equivalent, so it can be used and not extracted. Detection must live at the service provider, because the identity provider will have no record of the event; the signals are assertions whose properties do not match the identity provider’s real behaviour, such as unusual authentication context values or session indices matching no known session. And key rotation is a security control, not only a chore, because it bounds how long a stolen key stays useful.
A verification checklist that holds#
Everything above condenses into a list. If your SAML service provider does all of this, in this order, you have eliminated every attack class described in this chapter. If it skips any of them, name which one and know why.
- Impose a maximum size on the incoming message and reject anything larger before parsing.
- Parse with document type declarations disallowed, external entities disabled and network access disabled in the parser.
- Validate the document against the SAML schema, using the same parser and the same tree that will be used for everything that follows.
- Locate the assertion by position — as the expected child of the response, or as the sole content of an EncryptedAssertion — and refuse to search the document for it.
- Reject the message if it contains more than one assertion or more than one signature.
- Decrypt first if the assertion is encrypted, then verify the signature over the decrypted assertion.
- Verify the signature and require that its reference resolves to the assertion element you selected by position, not to anything else.
- Confirm the signing key is one of the keys currently in the identity provider’s metadata for that entity, compared as a key and not validated as a certificate chain.
- Extract every value from the verified assertion object, using the same parser and traversal, never from the original document.
- Check the issuer is the expected entity identifier, as an exact string.
- Check the audience restriction names your own entity identifier, as an exact string.
- Check the current time against Conditions NotBefore and NotOnOrAfter, and against SubjectConfirmationData NotOnOrAfter, with a small explicit clock tolerance.
- Check Recipient matches the assertion consumer service address the message actually arrived at, and Destination matches it too.
- Check InResponseTo matches a request you issued and have not yet consumed; if the flow is identity-provider-initiated, record that the check was unavailable.
- Check the assertion ID against a shared replay cache, then store it for at least the assertion lifetime plus your clock tolerance.
- Only now create a session, and never carry the assertion or its identifiers into it.
SAML against OpenID Connect, honestly#
Chapter 41 covers OpenID Connect properly, so this section confines itself to the comparison a practitioner actually has to make: given a new integration in August 2026, which do you choose, and when is the older protocol still the right answer.
| Question | SAML 2.0 | OpenID Connect |
|---|---|---|
| Token format | Signed XML | Signed JSON (JWT) |
| Canonicalization | Required | None |
| Native and mobile apps | Poor fit | Designed for it |
| Multi-valued attributes | Native | By convention |
| Key rotation | Metadata refresh | JWKS endpoint |
Start with the honest case for OpenID Connect, because it is strong. The entire class of attacks in the middle of this chapter — wrapping, comment truncation, parser differentials, external entities — exists because the token is XML and XML signatures sign a set of references inside a mutable tree. A JSON Web Token signs a fixed byte string carried base64-encoded; there is no canonicalization step, no reference resolution, no place for a second copy of the token to hide. That is not a difference of degree. It removes the mechanism. Add the rotating key endpoint that makes rollover a cache refresh rather than a federation-wide operation, the fit with mobile and single-page applications that redirect-and-form-post handles badly, and a much larger library ecosystem, and for a new consumer-facing or mobile integration the choice is not close.
The honest case for SAML is not merely inertia, though inertia is most of it.
Attribute richness is real. A SAML assertion carries multi-valued attributes as a first-class construct with an agreed shape, and enterprise directories are full of multi-valued things: group memberships, entitlements, affiliations. Doing the same in a JSON Web Token works, but the conventions are per-deployment, and a claim containing four hundred group names is a token that no longer fits comfortably in a header.
Element-level encryption is real. SAML can encrypt an individual assertion, or an individual attribute, so that an intermediary can route a message it cannot read. The 2011 CCS paper by Tibor Jager and Juraj Somorovsky, “How to Break XML Encryption”, is a reminder that this capability has its own sharp edges, but it has no direct equivalent in the OpenID Connect deployments most people run. Single logout is real in the same qualified sense: it is difficult to operate and patchily implemented, but a SessionIndex exists and a protocol for ending the session it names is specified.
Institutional entrenchment is the largest factor and deserves to be stated without disdain. Government procurement frameworks, health information exchanges and academic federations standardized on SAML, and their trust arrangements are legal instruments with signatures on paper, not configuration files. A protocol change is a contract renegotiation across hundreds of parties. That is why the realistic prediction for the rest of this decade is coexistence rather than replacement.
Experts genuinely disagree on one point, and it is worth stating both sides fairly. One camp holds that SAML’s vulnerability record is a library problem rather than a protocol problem — that a correctly written verifier is not hard to specify, that the checklist above is finite, and that the same engineering carelessness would produce equivalent bugs in any format. The other holds that a specification whose safe use requires sixteen ordered checks and a ban on re-parsing is a specification that has failed, regardless of whether any individual bug was avoidable, because the count of implementations that get it right is the measurement that matters. The evidence available in 2026 is uncomfortable for the first camp: the same class of defect reappeared in 2018 and again in 2025, in different languages, in libraries maintained by competent people.
The practical position that follows from both views is the same. Use OpenID Connect for anything new where you have the choice. Where you must use SAML, do not write the verification yourself, use a maintained library, pin it, watch its advisories, and hold it to the checklist.
42.98 Common wrong ideas#
Wrong: SAML is a dead protocol that OpenID Connect replaced. Right: The specification is finished and the OASIS committee that owned it closed on 8 July 2023, but the deployment is not finished; SAML still carries a large share of enterprise, government, health and academic login as of August 2026, and every major identity provider supports it as a first-class protocol alongside OpenID Connect.
Wrong: If the signature on a SAML response verifies, the login is authentic. Right: Signature verification answers only “some element in this document was signed by a key I trust”. It says nothing about whether that element is the one your application then reads, whether the assertion was addressed to you, whether it is still inside its validity window, or whether you have seen it before; those are separate checks and skipping any of them defeats the signature entirely.
Wrong: A valid XML signature proves the document was not modified. Right: An XML signature covers a set of referenced nodes after transformation, not the document. A document can be extensively restructured, have elements added, and have a forged assertion inserted, while a signature over an untouched relocated element still verifies. That is signature wrapping, documented across eleven of fourteen frameworks in the 2012 USENIX Security paper by Somorovsky and colleagues.
Wrong: The connection is TLS-protected between our servers, so the assertion signature is a formality. Right: The assertion does not travel between your servers; in the Web Browser SSO profile it travels through the user’s browser, which is outside your trust boundary. The signature is the only thing that survives that journey, and the Profiles specification requires the enclosed assertion to be signed when the HTTP-POST binding is used.
Wrong: Identity-provider-initiated SSO is the same thing with a nicer starting page. Right: It is the same flow with the request-response correlation removed. There is no InResponseTo value to match because there was no request, so a captured assertion becomes a complete, transferable credential for as long as its window lasts. It can be operated safely only with a short lifetime, an enforced replay cache and a strictly enforced audience restriction.
Wrong: An expired X.509 certificate in a partner’s SAML metadata must be rejected. Right: The SAML V2.0 Metadata Interoperability Profile of 4 August 2009 says the opposite: once metadata is accepted, a consumer must not apply further criteria to the keys inside it, explicitly excluding path validation, revocation lists and OCSP. The certificate is a container for a public key, and the trust comes from the metadata, not from a certificate authority.
Wrong: The entity identifier is a web address you can fetch to get the metadata. Right: It is an identifier that merely looks like a URI, restricted to 1024 characters, compared as an exact string and frequently not resolvable at all. A trailing slash or a scheme change makes it a different entity, and the resulting failure usually presents as a signature error rather than a naming error.
Wrong: Validating the document against the SAML schema prevents wrapping attacks. Right: Schema validation helps and does not close the class. The schema deliberately permits arbitrary content in extension points such as the response Extensions element, which is exactly where a relocated signed assertion is hidden, and in many implementations the parser used for validation is not the parser used for extraction, which reintroduces the gap as a parser differential.
Wrong: External entity attacks do not matter because the forged message will fail signature verification anyway. Right: The parse happens before the verification, necessarily, because you cannot find a signature in a document you have not parsed. By the time verification fails, the file has already been read into the document and will often escape through an error message or a log line. CVE-2026-28809 in the esaml library is a 2026 example of exactly this ordering.
Wrong: Clock skew between the identity provider and the service provider is fixed by making assertions valid for longer. Right: Skew is fixed by time synchronization and a small explicit tolerance of roughly thirty to sixty seconds at the window boundaries. Extending the assertion lifetime to hours converts a synchronization problem into a replay window of the same length, during which a captured bearer assertion is a working credential.
42.99 Chapter summary in 20 lines#
- SAML’s specifications were approved as an OASIS Standard in March 2005 and the committee that owned them closed on 8 July 2023.
- It is built in four layers: assertions and protocols in the Core document, bindings in Bindings, and named end-to-end recipes in Profiles.
- An assertion is a signed statement about a subject, carrying an identifier, a validity window, an audience, an authentication event and attributes.
- Subject says who the assertion is about; SubjectConfirmation says how the recipient may confirm the presenter is that subject.
- Browser SSO uses bearer confirmation, which means possession is proof, so a captured assertion is a working credential until its window closes.
- Conditions carries NotBefore, NotOnOrAfter and the AudienceRestriction naming the intended service provider by exact entity identifier.
- AuthnStatement records when the subject was checked, by what method, and under which identity provider session index.
- HTTP-Redirect compresses the message with raw DEFLATE into a query string and signs the concatenated query parameters rather than the XML.
- HTTP-POST carries base64-encoded XML in a self-submitting form and is what nearly every deployment uses for the response.
- HTTP-Artifact sends a 44-byte reference resolved over a direct back-channel SOAP call, keeping the assertion out of the browser entirely.
- Service-provider-initiated login matches InResponseTo against a request actually issued; identity-provider-initiated login removes that check.
- Signing the response is not a substitute for signing the assertion, because an unsigned assertion loses all protection once it is moved.
- Signature wrapping relocates the genuine signed assertion where the verifier still finds it, while a forged one occupies the position the application reads.
- The fix is structural: select by position, reject multiple assertions, and read every value from the verified object rather than the document.
- Canonicalization strips comments so they cannot break a signature, which is what allowed the 2018 comment-truncation attacks in CERT note VU#475445.
- The same defect returned in March 2025 as parser differentials in ruby-saml and as SAMLStorm in xml-crypto.
- External entity attacks succeed because an untrusted document must be parsed before its signature can be found, so doctype declarations must be disallowed.
- Metadata is trusted as a whole, and the Metadata Interoperability Profile forbids path validation, expiry or revocation checks on the keys inside it.
- Certificate rollover is therefore scheduling: publish both keys, wait past the longest cache duration, confirm, switch, wait again, remove the old one.
- As of August 2026 the honest position is coexistence: OpenID Connect for anything new, SAML where federations and procurement rules require it.
Chapter sources: OASIS Security Assertion Markup Language V2.0, approved as an OASIS Standard in March 2005 with specification documents dated 15 March 2005, specifically Assertions and Protocols (Core) sections 2.3.3, 2.4.1, 2.4.1.1, 2.4.1.2, 2.5.1, 2.5.1.5, 2.7.2, 2.7.3, 5.4.2, 5.4.3 and 5.4.4; Bindings sections 3.2, 3.3, 3.4, 3.4.4.1, 3.5, 3.6, 3.6.4.2 and 3.7; Profiles sections 4.1, 4.1.4.1 to 4.1.4.5, 4.1.5, 4.1.6 and 4.2; Metadata sections 2.2.1, 2.3.1, 2.3.2, 2.4.1, 2.4.1.1, 2.4.3 and 2.4.4; and the Security and Privacy Considerations document, together with the approved SAML V2.0 errata posted 1 May 2012 following the public review draft of 21 February 2012, in particular erratum E83 on exclusive canonicalization; the SAML V2.0 Metadata Interoperability Profile Version 1.0, OASIS Committee Specification 01, 4 August 2009; the OASIS Security Services Technical Committee closure notice of 8 July 2023; W3C Canonical XML Version 1.0 (Recommendation, March 2001) and Exclusive XML Canonicalization Version 1.0 (Recommendation, 18 July 2002); Juraj Somorovsky, Andreas Mayer, Joerg Schwenk, Marco Kampmann and Meiko Jensen, “On Breaking SAML: Be Whoever You Want to Be”, 21st USENIX Security Symposium, Bellevue, Washington, August 2012, reporting eleven of fourteen frameworks vulnerable; Alessandro Armando and colleagues, “Formal Analysis of SAML 2.0 Web Browser Single Sign-On: Breaking the SAML-based Single Sign-On for Google Apps”, FMSE 2008, Alexandria, Virginia, 27 October 2008, and CERT vulnerability note VU#612636; Tibor Jager and Juraj Somorovsky, “How to Break XML Encryption”, ACM CCS 2011; Kelby Ludwig of Duo Labs, “Duo Finds SAML Vulnerabilities Affecting Multiple Implementations”, 27 February 2018, and CERT vulnerability note VU#475445 of the same date covering CVE-2017-11427, CVE-2017-11428, CVE-2017-11429, CVE-2017-11430, CVE-2018-0489 and CVE-2018-5387; GitHub Security Lab, “Sign in as anyone: bypassing SAML SSO authentication with parser differentials”, March 2025, covering CVE-2025-25291 and CVE-2025-25292 in ruby-saml, fixed in 1.18.0 and 1.12.4; WorkOS, “SAMLStorm”, 14 March 2025, covering CVE-2025-29774 and CVE-2025-29775 in xml-crypto version 6.0.0 and earlier with fixes in 6.0.1, 3.2.1 and 2.1.6, and the related node-saml advisory CVE-2025-54419; NVD record CVE-2026-28809 on XML external entity processing in esaml, published March 2026 and updated July 2026; Shaked Reiner of CyberArk, “Golden SAML”, November 2017, and the December 2020 SolarWinds intrusion reporting on forged SAML tokens; and the Internet-Drafts draft-young-md-query and draft-young-md-query-saml, the latter at revision 25 dated June 2026 and still not published as an RFC as of August 2026.