Short answer. Validating a SAML response has two halves, and most incidents come from doing only the first. First, verify the XML signature with the identity provider's certificate taken from its metadata, never from the KeyInfo in the message, and require exactly one Reference that points at the ID of the Assertion (or of the Response) you are about to use. Second, read every value you rely on (the NameID, attributes, Issuer, Audience, Recipient, times) from the element that signature covers, not by searching the document you received: an XML document can contain an unsigned look-alike element next to the signed one. Then apply the SAML rules: the audience is your entity ID, the bearer SubjectConfirmationData names your assertion consumer URL and your request, nothing has expired, and each assertion ID is accepted once.
How an XML signature points at what it signs
An XML signature (XML Signature Syntax and Processing 1.1) does not sign a document as bytes. It signs a SignedInfo element, which lists one or more References. Each Reference has a URI that selects content, a list of Transforms applied to it, and a DigestValue of the result. A SAML signature, as issued by the lab identity provider below and printed with the long base64 values shortened:
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_ef6e7d7e83384141a7a16e6fb9d52d61" Version="2.0" IssueInstant="2026-10-20T13:39:36Z" Destination="https://sp.example/saml/acs" InResponseTo="_req-7d1f">
<saml:Issuer>https://idp.example/saml</saml:Issuer>
<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
<saml:Assertion ID="_cf908bd7284743f8997ddd7943f6640c" Version="2.0" IssueInstant="2026-10-20T13:39:36Z">
<saml:Issuer>https://idp.example/saml</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#ecdsa-sha256"/><ds:Reference URI="#_cf908bd7284743f8997ddd7943f6640c"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>genlRcOfIZpWA8BHZ0Eo42LeZcRMriskgTApPDxsPI0=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>cPF5v/9Ve6uUmgcn...</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIBPDCB4qADAgEC...</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">alice@example.com</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2026-10-20T13:44:36Z" Recipient="https://sp.example/saml/acs" InResponseTo="_req-7d1f"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2026-10-20T13:39:36Z" NotOnOrAfter="2026-10-20T13:44:36Z">
<saml:AudienceRestriction><saml:Audience>https://sp.example/saml</saml:Audience></saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-10-20T13:39:36Z"><saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</saml:AuthnContext></saml:AuthnStatement>
</saml:Assertion></samlp:Response>
Three details decide what is protected:
URI="#_cf90…"is a same-document reference: it selects the element whoseIDattribute has that value, with its descendants (XMLDSig §4.4.3.2). Where that element sits in the document is not part of the signature.- The enveloped-signature transform removes the
ds:Signatureelement itself before digesting, so the signature can live inside what it signs. - Exclusive canonicalization (
xml-exc-c14n#, here without comments) turns the selected element into a canonical byte string: attribute order, namespace declarations and whitespace inside tags are normalized, and comments are dropped. Different documents can therefore have the same canonical form.
Validation is "reference validation" (recompute each digest) followed by "signature validation" over the canonicalized SignedInfo (XMLDSig §3.2). It answers one question: these canonical bytes were signed by this key. It says nothing about the rest of the document. The specification puts it in a section title, "Only What is Signed is Secure" (§8.1.1), and adds that applications should operate on the data "that was transformed (including canonicalization) and signed" (§8.1.3).
What the SAML specifications require
SAML narrows XMLDSig down (SAML Core 2.0 §5.4): signatures MUST be enveloped; they MUST contain "a single <ds:Reference> containing a same-document reference to the ID attribute value of the root element of the assertion or protocol message being signed"; exclusive canonicalization SHOULD be used; and transforms other than enveloped-signature and exclusive canonicalization SHOULD NOT appear. Verifiers that accept other transforms "MUST ensure that no content of the SAML message is excluded from the signature".
The Web Browser SSO profile (SAML Profiles 2.0 §4.1.4) adds the checks that make a valid signature mean "this user, for this service, now":
- With the HTTP-POST binding, "the enclosed assertion(s) MUST be signed" (§4.1.4.5). A signed
Responsearound an unsigned assertion does not satisfy this. - The service provider MUST verify any signatures, that
Recipientin the bearerSubjectConfirmationData"matches the assertion consumer service URL", that itsNotOnOrAfter"has not passed, subject to allowable clock skew", and thatInResponseToequals the ID of itsAuthnRequest, or is absent for an unsolicited response (§4.1.4.3). - It MUST "ensure that bearer assertions are not replayed, by maintaining the set of used ID values" for as long as the assertion would be valid (§4.1.4.5).
- The
AudienceRestrictioncondition "evaluates to Valid if and only if the SAML relying party is a member of one or more of the audiences specified" (SAML Core §2.5.1.4), and theConditionsNotBeforeandNotOnOrAfterbound the assertion's validity (§2.5.1.2).
A validator that reads only signed data
The validator below uses signxml 5.2.0 for the XML signature. signxml's own documentation calls the rule "See what is signed" and makes it the API: verify() returns the signed element as signed_xml, with comments that the signature did not cover removed, and location pins where ds:Signature must be. Everything after the signature check reads from result.signed_xml:
# Service provider side: verify the signature, then read every value from the signed Assertion only.
import datetime
from cryptography import x509
from signxml import SignatureConfiguration, XMLVerifier
SAMLP = "urn:oasis:names:tc:SAML:2.0:protocol"
SAML = "urn:oasis:names:tc:SAML:2.0:assertion"
NS = {"samlp": SAMLP, "saml": SAML}
BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
class SamlError(Exception):
pass
def _time(value):
return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc)
def validate_response(xml: bytes, *, idp_cert: x509.Certificate, idp_entity_id: str, sp_entity_id: str,
acs_url: str, request_id: str, seen_assertion_ids: set, skew=60, now=None):
now = now or datetime.datetime.now(datetime.timezone.utc)
lag = datetime.timedelta(seconds=skew)
# 1. The signature: pinned IdP certificate from metadata (never the one in KeyInfo),
# exactly one Reference, ds:Signature where SAML puts it: inside the Assertion.
config = SignatureConfiguration(location=f"./{{{SAML}}}Assertion/", expect_references=1)
try:
result = XMLVerifier().verify(xml, x509_cert=idp_cert, expect_config=config)
except Exception as e: # signxml raises InvalidSignature, InvalidInput and others
raise SamlError(f"signature: {type(e).__name__}: {e}") from None
# 2. From here on, only the element the signature covers. Never the document passed in.
assertion = result.signed_xml
if assertion.tag != f"{{{SAML}}}Assertion":
raise SamlError(f"signed element is {assertion.tag}, not an Assertion")
aid = assertion.get("ID")
one = lambda path: assertion.find(path, NS)
if (one("saml:Issuer").text or "").strip() != idp_entity_id:
raise SamlError("issuer")
conditions = one("saml:Conditions")
if conditions is None:
raise SamlError("no Conditions")
if now + lag < _time(conditions.get("NotBefore")) or now - lag >= _time(conditions.get("NotOnOrAfter")):
raise SamlError("assertion is outside its validity window")
audiences = [a.text for a in conditions.findall("saml:AudienceRestriction/saml:Audience", NS)]
if sp_entity_id not in audiences:
raise SamlError(f"audience {audiences} is not this SP")
# 3. A bearer SubjectConfirmation for this ACS URL, this request, still valid.
data = one(f"saml:Subject/saml:SubjectConfirmation[@Method='{BEARER}']/saml:SubjectConfirmationData")
if data is None:
raise SamlError("no bearer SubjectConfirmationData")
if data.get("Recipient") != acs_url:
raise SamlError(f"recipient {data.get('Recipient')}")
if now - lag >= _time(data.get("NotOnOrAfter")):
raise SamlError("subject confirmation expired")
if data.get("InResponseTo") != request_id:
raise SamlError("InResponseTo does not match the AuthnRequest")
# 4. One use per assertion ID, for as long as the assertion could be accepted.
if aid in seen_assertion_ids:
raise SamlError(f"assertion {aid} already used")
seen_assertion_ids.add(aid)
name_id = one("saml:Subject/saml:NameID")
return {"name_id": "".join(name_id.itertext()), "assertion_id": aid}
For comparison, this is the shape of the bug. It calls the same signature check, discards the result, and then reads the first Assertion it finds in the document:
# Anti-pattern, for comparison only: the signature is checked, but the values come from the
# document as received instead of from the element the signature covers.
from lxml import etree
from signxml import SignatureConfiguration, XMLVerifier
NS = {"samlp": "urn:oasis:names:tc:SAML:2.0:protocol", "saml": "urn:oasis:names:tc:SAML:2.0:assertion"}
def naive_validate(xml: bytes, *, idp_cert, sp_entity_id: str, acs_url: str, request_id: str):
config = SignatureConfiguration(location="./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion/", expect_references=1)
XMLVerifier().verify(xml, x509_cert=idp_cert, expect_config=config) # result ignored
doc = etree.fromstring(xml)
assertion = doc.find("saml:Assertion", NS) # the first Assertion in the document
assert sp_entity_id in [a.text for a in assertion.findall(".//saml:Audience", NS)]
data = assertion.find(".//saml:SubjectConfirmationData", NS)
assert data.get("Recipient") == acs_url and data.get("InResponseTo") == request_id
return {"name_id": assertion.find("saml:Subject/saml:NameID", NS).text}
Tested against manipulated responses
A lab identity provider generates a P-256 key and a self-signed certificate per run and issues signed responses; the service provider pins that certificate:
# Lab identity provider: a key and self-signed certificate generated per run, and signed SAML Responses.
import datetime
import uuid
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
from lxml import etree
from signxml import XMLSigner
from signxml.algorithms import CanonicalizationMethod, SignatureMethod
SAMLP = "urn:oasis:names:tc:SAML:2.0:protocol"
SAML = "urn:oasis:names:tc:SAML:2.0:assertion"
def new_idp(name):
key = ec.generate_private_key(ec.SECP256R1())
subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)])
now = datetime.datetime.now(datetime.timezone.utc)
cert = (
x509.CertificateBuilder()
.subject_name(subject).issuer_name(subject).public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(minutes=5)).not_valid_after(now + datetime.timedelta(days=30))
.sign(key, hashes.SHA256())
)
return key, cert
def iso(t):
return t.strftime("%Y-%m-%dT%H:%M:%SZ")
def response(key, cert, *, name_id, audience, recipient, in_response_to, issuer="https://idp.example/saml",
valid_for=300, now=None):
"""A Response with one signed Assertion: enveloped signature, exclusive C14N, Reference URI=#ID."""
now = now or datetime.datetime.now(datetime.timezone.utc)
until = now + datetime.timedelta(seconds=valid_for)
aid = f"_{uuid.uuid4().hex}"
xml = f"""<samlp:Response xmlns:samlp="{SAMLP}" xmlns:saml="{SAML}" ID="_{uuid.uuid4().hex}" Version="2.0"
IssueInstant="{iso(now)}" Destination="{recipient}" InResponseTo="{in_response_to}">
<saml:Issuer>{issuer}</saml:Issuer>
<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
<saml:Assertion ID="{aid}" Version="2.0" IssueInstant="{iso(now)}">
<saml:Issuer>{issuer}</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">{name_id}</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="{iso(until)}" Recipient="{recipient}" InResponseTo="{in_response_to}"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="{iso(now)}" NotOnOrAfter="{iso(until)}">
<saml:AudienceRestriction><saml:Audience>{audience}</saml:Audience></saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="{iso(now)}"><saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</saml:AuthnContext></saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>"""
root = etree.fromstring(xml.encode())
assertion = root.find(f"{{{SAML}}}Assertion")
signed = XMLSigner(
signature_algorithm=SignatureMethod.ECDSA_SHA256,
c14n_algorithm=CanonicalizationMethod.EXCLUSIVE_XML_CANONICALIZATION_1_0,
).sign(assertion, key=key, cert=[cert], reference_uri=aid)
# SAML requires ds:Signature right after saml:Issuer inside the signed element.
sig = signed.find("{http://www.w3.org/2000/09/xmldsig#}Signature")
signed.remove(sig)
signed.insert(1, sig)
root.replace(assertion, signed)
return etree.tostring(root)
The driver sends ten responses to both functions: a valid one, a fresh one that is then replayed, and eight manipulated or unacceptable ones:
# Lab driver: valid and manipulated SAML Responses against the validator (sp.py) and the anti-pattern (naive.py).
import copy
import datetime
import re
from lxml import etree
from idp import SAML, new_idp, response
from naive import naive_validate
from sp import SamlError, validate_response
key, cert = new_idp("idp.example signing")
other_key, other_cert = new_idp("idp.example signing") # same name, different key: not the IdP
SP = {"sp_entity_id": "https://sp.example/saml", "acs_url": "https://sp.example/saml/acs", "request_id": "_req-7d1f"}
seen = set()
def issue(name_id="alice@example.com", **kw):
args = {"audience": SP["sp_entity_id"], "recipient": SP["acs_url"], "in_response_to": SP["request_id"], **kw}
return response(args.pop("key", key), args.pop("cert", cert), name_id=name_id, **args)
def check(label, xml, **kw):
try:
ours = validate_response(xml, idp_cert=cert, idp_entity_id="https://idp.example/saml",
seen_assertion_ids=seen, **SP, **kw)
ours = f"login as {ours['name_id']}"
except SamlError as e:
ours = f"rejected: {e}"
try:
naive = f"login as {naive_validate(xml, idp_cert=cert, **SP)['name_id']}"
except Exception as e:
naive = f"rejected: {type(e).__name__}"
print(f"{label}\n validator: {ours}\n anti-pattern: {naive}")
check("valid response", issue())
# Signature wrapping: an unsigned Assertion inserted before the signed one. The signature still
# verifies, because its Reference points by ID at the original Assertion.
doc = etree.fromstring(issue())
signed = doc.find(f"{{{SAML}}}Assertion")
forged = copy.deepcopy(signed)
forged.remove(forged.find("{http://www.w3.org/2000/09/xmldsig#}Signature"))
forged.set("ID", "_forged")
forged.find(f"{{{SAML}}}Subject/{{{SAML}}}NameID").text = "admin@example.com"
doc.insert(list(doc).index(signed), forged)
check("unsigned Assertion before the signed one", etree.tostring(doc))
# A comment inside the signed NameID: exclusive C14N without comments ignores it, so the
# signature stays valid; .text on the received document stops at the comment.
xml = issue("admin@example.com.evil.test")
xml = xml.replace(b"admin@example.com.evil.test<", b"admin@example.com<!---->.evil.test<")
check("comment inside the signed NameID", xml)
check("assertion for another SP", issue(audience="https://other-sp.example/saml"))
check("expired 10 minutes ago", issue(now=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=15)))
replay = issue()
check("first use", replay)
check("same assertion again", replay)
tampered = issue().replace(b"alice@example.com", b"admin@example.com")
check("NameID edited after signing", tampered)
check("signed with another key + its cert", issue(key=other_key, cert=other_cert))
stripped = re.sub(rb"<ds:Signature.*?</ds:Signature>", b"", issue(), flags=re.S)
check("signature removed", stripped)
valid response
validator: login as alice@example.com
anti-pattern: login as alice@example.com
unsigned Assertion before the signed one
validator: login as alice@example.com
anti-pattern: login as admin@example.com
comment inside the signed NameID
validator: login as admin@example.com.evil.test
anti-pattern: login as admin@example.com
assertion for another SP
validator: rejected: audience ['https://other-sp.example/saml'] is not this SP
anti-pattern: rejected: AssertionError
expired 10 minutes ago
validator: rejected: assertion is outside its validity window
anti-pattern: login as alice@example.com
first use
validator: login as alice@example.com
anti-pattern: login as alice@example.com
same assertion again
validator: rejected: assertion _2291663e82ca413bb791278fa3a9a3f8 already used
anti-pattern: login as alice@example.com
NameID edited after signing
validator: rejected: signature: InvalidDigest: Digest mismatch for reference 0 (#_37ad00c1109f47e3b3b1b650a17aba70)
anti-pattern: rejected: InvalidDigest
signed with another key + its cert
validator: rejected: signature: InvalidSignature: Signature verification failed:
anti-pattern: rejected: InvalidSignature
signature removed
validator: rejected: signature: InvalidInput: Expected to find XML element Signature in {urn:oasis:names:tc:SAML:2.0:protocol}Response using XPath ./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion/ds:Signature
anti-pattern: rejected: InvalidInput
Reading it case by case:
- An unsigned assertion before the signed one. The signature still verified, because its reference selects the original assertion by ID wherever it is. The anti-pattern read the first
Assertionin the document and logged in a user the identity provider never vouched for. The validator usedsigned_xmland saw only the original. This family of bugs is usually called signature wrapping. - A comment inside the signed
NameID. The identity provider signedadmin@example.com.evil.test, an account its user could legitimately have. Inserting<!---->afteradmin@example.comdoes not change the canonical form, because exclusive canonicalization without comments drops comments, so the signature stays valid. In lxml,.textreturns only the text before the first child node, so the anti-pattern readadmin@example.com. signxml returns the signed element without the comment, and the validator joins all text nodes; it saw the real account. This exact pattern, published in February 2018 as CERT/CC VU#475445 after research by Duo Labs, affected OneLogin's python-saml and ruby-saml, saml2-js, OmniAuth-SAML and Shibboleth's OpenSAML C++, among others. - An assertion for another service provider, signed by the same identity provider, failed the audience check. Both implementations caught it, because the anti-pattern happened to check the audience.
- Expiry and replay were caught only by the validator. Both are SAML rules, not signature rules: the signature on a captured assertion stays valid forever.
- Edited, re-signed and unsigned responses failed at the signature in both: digest mismatch, a key that is not the pinned one even though its certificate was embedded in
KeyInfoand carried the same subject name, and no signature wherelocationrequires one.
Details that decide real deployments
Response signature or assertion signature. Identity providers sign the assertion, the response, or both. If only the Response is signed, the assertion is covered as a descendant, and the rule becomes: use the assertion that is a child of the signed response element. The POST profile still requires signed assertions, so configure the identity provider to sign them. Never accept "a signature somewhere in the document".
Encrypted assertions. An EncryptedAssertion must be decrypted with your key first; the decrypted Assertion must then carry a valid signature by the identity provider. Encryption only proves the sender knew your public key.
Trust comes from metadata. The identity provider's signing certificates belong in your configuration, imported from its metadata and updated when it rotates them; overlapping certificates during a rollover are normal. A certificate inside the message proves nothing: anyone can create one with any subject name, which is what the "another key" case did.
Algorithms. SAML Core's text from 2005 names RSA with SHA-1; signxml 5.2.0 rejects SHA-1 signature and digest methods by default, and you should too. Restrict accepted methods to what your identity provider uses, typically RSA-SHA256 or ECDSA-SHA256.
XML parsing. Parse with external entities and DTDs disabled before anything else; signxml's parser sets resolve_entities=False. Reject documents with more than one element carrying the same ID.
Times. Allow a small skew (seconds to a couple of minutes) and store used assertion IDs until their NotOnOrAfter plus that skew.
When a legitimate response fails
The opposite problem, a real response from your identity provider that will not verify, has a short list of usual causes:
- The identity provider rotated its certificate and your configuration still holds the old one. Refresh from its metadata, which normally lists the new certificate before it is used.
- Something reformatted the XML. Exclusive canonicalization normalizes markup, not text: added indentation or line breaks between elements change text nodes inside the signed element and break the digest. Verify the bytes exactly as decoded from the form field; never pretty-print, re-serialize through another library or "clean up" before verification.
- The wrong decoding. With the HTTP-POST binding the
SAMLResponseform field is base64 of the XML; it may be line-wrapped (SAML Bindings §3.5.4). With HTTP-Redirect the message is DEFLATE-compressed, its XML signature is removed, and a separate signature over the query string travels in theSigAlgandSignatureparameters (§3.4.4.1); that signature is verified over the URL-encoded parameters, not the XML. Responses carrying signed assertions should not use the Redirect binding at all. - The signature is where your validator does not expect it. If the identity provider signs the
Responseand your configuration requires the signature inside theAssertion(or the reverse), verification fails. Decide which one you require and configure the identity provider to match; do not loosen the location to "anywhere". - Clocks. "Not yet valid" right after login means the service provider's clock is behind; widen the skew only after fixing the clock.
Libraries
Use a SAML library rather than a general XML signature library plus your own glue, keep it current, and read its documentation for the function that returns signed content. Versions current on 20 October 2026:
| Language | SAML library | XML signature layer |
|---|---|---|
| Python | python3-saml 1.16.0, pysaml2 7.5.5 | xmlsec 1.3.17, signxml 5.2.0 |
| Node.js | @node-saml/node-saml 5.1.0, samlify 2.15.0 | xml-crypto 6.4.0 |
| Go | crewjam/saml v0.5.1 | russellhaering/goxmldsig v1.6.1 |
xml-crypto's README spells out the same rule as signxml: after checkSignature() succeeds, use the canonical XML from getSignedReferences(), and do not take nodes from the original document; getReferences() and validateElementAgainstReferences() are deprecated for that purpose. If you maintain code that verifies and then queries the input document, that is the line to change.
Checklist
- Parse safely: no DTDs, no external entities, no duplicate
IDs. - Verify the signature with the pinned certificate from the identity provider's metadata; ignore
KeyInfofor trust. - Require exactly one
Reference, pointing at the element you will use, with only enveloped-signature and exclusive canonicalization transforms, and accepted algorithms only. - From the signed element only:
Issuerequals the identity provider's entity ID;Conditionstime window andAudienceRestrictionwith your entity ID; a bearerSubjectConfirmationDatawith your ACS URL asRecipient, a futureNotOnOrAfter, and your request ID asInResponseTo(absent only for unsolicited responses you have decided to accept). - Record the assertion ID and reject it if it comes again while still valid.
- Read the
NameIDand attributes from the signed element, joining all text nodes, and map users byIssuerplusNameID. - Test with an expired and a replayed assertion and a certificate that is not the identity provider's: all must fail. Test a wrapped response and a commented
NameIDtoo: they must log in only the identity the provider signed, in full.
Sources
- XML Signature Syntax and Processing 1.1: §3.2 (core validation), §4.4.3 (references), §6.1 (algorithms), §8.1 (security considerations)
- SAML Core 2.0: §2.5.1 (conditions), §5.4 (XML signature profile)
- SAML Profiles 2.0: §4.1.4 (Web Browser SSO processing rules)
- SAML Bindings 2.0: §3.4.4.1 (Redirect, DEFLATE), §3.5.4 (POST)
- signxml documentation: "See what is signed"
- xml-crypto README:
getSignedReferences() - CERT/CC VU#475445: Multiple SAML libraries may allow authentication bypass via incorrect XML canonicalization and DOM traversal (2018)