SAML Security: Detection, Analysis & Prevention Guide
Table of Contents
1. Introduction to SAML Security
Security Assertion Markup Language (SAML) is an XML-based open standard for exchanging authentication and authorization data between an Identity Provider (IdP) and a Service Provider (SP). SAML 2.0, the most widely deployed version, is the backbone of enterprise Single Sign-On (SSO) — used by Okta, Azure AD, OneLogin, PingFederate, and thousands of SaaS applications.
Why SAML Vulnerabilities Are Critical
SAML vulnerabilities are among the most severe in enterprise security. A successful SAML attack typically results in authentication bypass — the attacker logs in as any user (including admin) without knowing their password. Because SAML assertions are XML documents with cryptographic signatures, the attack surface combines XML parsing flaws, cryptographic verification bugs, and protocol logic errors. A single bug in SAML validation has led to full account takeover in products from GitHub, Slack, GitLab, and many identity providers.
In this guide, you'll learn how SAML SSO works and where it is vulnerable, how XML Signature Wrapping (XSW) attacks bypass authentication, what assertion validation mistakes lead to account takeover, how to detect SAML flaws during code review, and what prevention patterns actually work.
SAML SSO Authentication Flow
SP-Initiated SSO Flow
Where Attacks Target This Flow
Anatomy of a SAML Response
1<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
2 ID="_response123" InResponseTo="_request456"
3 Destination="https://app.example.com/saml/acs"
4 IssueInstant="2025-01-15T10:30:00Z">
5 <saml:Issuer>https://idp.corp.com</saml:Issuer>
6
7 <!-- The XML Digital Signature covers the Assertion -->
8 <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9 <ds:SignedInfo>
10 <ds:Reference URI="#_assertion789">
11 <!-- Digest of the Assertion element -->
12 <ds:DigestValue>abc123...</ds:DigestValue>
13 </ds:Reference>
14 </ds:SignedInfo>
15 <ds:SignatureValue>xyz789...</ds:SignatureValue>
16 </ds:Signature>
17
18 <saml:Assertion ID="_assertion789"
19 IssueInstant="2025-01-15T10:30:00Z">
20 <saml:Issuer>https://idp.corp.com</saml:Issuer>
21 <saml:Subject>
22 <saml:NameID>admin@corp.com</saml:NameID>
23 <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
24 <saml:SubjectConfirmationData
25 NotOnOrAfter="2025-01-15T10:35:00Z"
26 Recipient="https://app.example.com/saml/acs"
27 InResponseTo="_request456" />
28 </saml:SubjectConfirmation>
29 </saml:Subject>
30 <saml:Conditions NotBefore="2025-01-15T10:29:00Z"
31 NotOnOrAfter="2025-01-15T10:35:00Z">
32 <saml:AudienceRestriction>
33 <saml:Audience>https://app.example.com</saml:Audience>
34 </saml:AudienceRestriction>
35 </saml:Conditions>
36 <saml:AuthnStatement AuthnInstant="2025-01-15T10:29:30Z">
37 <saml:AuthnContext>
38 <saml:AuthnContextClassRef>
39 urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
40 </saml:AuthnContextClassRef>
41 </saml:AuthnContext>
42 </saml:AuthnStatement>
43 <saml:AttributeStatement>
44 <saml:Attribute Name="role">
45 <saml:AttributeValue>admin</saml:AttributeValue>
46 </saml:Attribute>
47 </saml:AttributeStatement>
48 </saml:Assertion>
49</samlp:Response>A developer says SAML is safe because 'the assertions are digitally signed by the IdP, so they can't be tampered with.' Why is this an incomplete view?
2. XML Signature Wrapping (XSW) Attacks
XML Signature Wrapping (XSW) is the most common and devastating class of SAML attacks. It exploits a fundamental disconnect in XML Digital Signatures: the signature references a specific element by its ID attribute, but the application may extract identity data from a different element at an expected position in the document tree.
How XSW Works
In an XSW attack, the attacker: (1) intercepts a valid SAML Response with a signed assertion, (2) moves the original signed assertion to a location where the SP's signature verification finds it (by ID reference) but the application logic ignores it, (3) inserts a new unsigned assertion with a forged identity (e.g., admin@corp.com) in the position where the SP extracts the user identity. The signature verifies against the original assertion, but the SP uses the forged one.
XSW Attack Variant 1: Wrapping the Assertion
1<!-- ORIGINAL signed SAML Response (legitimate) -->
2<samlp:Response>
3 <saml:Assertion ID="_legit123">
4 <!-- Signed assertion: user@corp.com -->
5 <saml:Subject>
6 <saml:NameID>user@corp.com</saml:NameID>
7 </saml:Subject>
8 </saml:Assertion>
9 <ds:Signature>
10 <ds:SignedInfo>
11 <ds:Reference URI="#_legit123" /> <!-- References _legit123 -->
12 </ds:SignedInfo>
13 </ds:Signature>
14</samlp:Response>
15
16<!-- AFTER XSW Attack: Attacker restructures the XML -->
17<samlp:Response>
18 <!-- EVIL: Unsigned assertion at the position SP reads -->
19 <saml:Assertion ID="_evil">
20 <saml:Subject>
21 <saml:NameID>admin@corp.com</saml:NameID>
22 </saml:Subject>
23 </saml:Assertion>
24
25 <!-- Original signed assertion moved to a wrapper element -->
26 <samlp:Extensions>
27 <saml:Assertion ID="_legit123">
28 <saml:Subject>
29 <saml:NameID>user@corp.com</saml:NameID>
30 </saml:Subject>
31 </saml:Assertion>
32 <ds:Signature>
33 <ds:SignedInfo>
34 <ds:Reference URI="#_legit123" />
35 </ds:SignedInfo>
36 </ds:Signature>
37 </samlp:Extensions>
38</samlp:Response>
39
40<!-- Result:
41 1. SP verifies signature → finds _legit123 by ID → VALID ✓
42 2. SP extracts identity → reads FIRST <Assertion> → admin@corp.com
43 3. Attacker is logged in as admin! -->XSW Attack Variant 2: Duplicated Assertion with different ID
1<!-- XSW Variant: Assertion duplication -->
2<samlp:Response>
3 <!-- Forged assertion (unsigned, read by application) -->
4 <saml:Assertion ID="_forged">
5 <saml:Subject>
6 <saml:NameID>admin@corp.com</saml:NameID>
7 </saml:Subject>
8 </saml:Assertion>
9
10 <!-- Original signed assertion (verified by signature check) -->
11 <saml:Assertion ID="_legit123">
12 <saml:Subject>
13 <saml:NameID>user@corp.com</saml:NameID>
14 </saml:Subject>
15 <ds:Signature>
16 <ds:SignedInfo>
17 <ds:Reference URI="#_legit123" />
18 </ds:SignedInfo>
19 </ds:Signature>
20 </saml:Assertion>
21</samlp:Response>
22
23<!-- Vulnerable SPs that:
24 - Verify the signature (finds _legit123 → valid)
25 - But use the FIRST assertion for identity (_forged → admin)
26 are compromised -->Known XSW Variants
| Variant | Technique | Moved Element | Impact |
|---|---|---|---|
| XSW1 | Wrap signed assertion in Extensions | Original assertion → <Extensions> | Unsigned assertion at top is used |
| XSW2 | Detach signature, insert before assertion | Signature detached from assertion | Forged assertion placed before signed one |
| XSW3 | Inject assertion inside signed assertion | Forged assertion nested as child | Inner assertion extracted by XPath |
| XSW4 | Wrap original in Object element | Original assertion → <ds:Object> | Forged assertion at expected position |
| XSW5 | Move signature, copy assertion value | Signature value to new location | Modified assertion at original position |
| XSW6 | Insert into Advice element | Original → <saml:Advice> | Forged assertion at document root |
| XSW7 | Nested Extensions wrapping | Multiple layers of wrapping | Bypasses simple tree-position checks |
| XSW8 | Signature element wrapping | Original assertion inside <ds:Object> | Bypasses namespace-aware parsers |
An SP verifies the SAML signature, then extracts the NameID using XPath: `//saml:Assertion/saml:Subject/saml:NameID`. The signature is valid. Is this secure?
3. Assertion Validation Vulnerabilities
Beyond signature wrapping, SAML implementations frequently fail to validate critical assertion properties. Each missing check opens a different attack vector.
Common assertion validation failures
1# ❌ VULNERABLE: No audience validation
2def process_saml_response(saml_response):
3 assertion = parse_assertion(saml_response)
4 # Checks signature — good
5 verify_signature(assertion, idp_cert)
6 # Extracts user — but NEVER checks Audience
7 return assertion.subject.name_id
8 # An assertion issued for app-A.com can be replayed to app-B.com
9 # if both trust the same IdP
10
11# ❌ VULNERABLE: No time validation
12def process_saml_response(saml_response):
13 assertion = parse_assertion(saml_response)
14 verify_signature(assertion, idp_cert)
15 # Never checks NotBefore / NotOnOrAfter
16 return assertion.subject.name_id
17 # Stolen assertions can be replayed indefinitely
18
19# ❌ VULNERABLE: No Recipient check
20def process_saml_response(saml_response):
21 assertion = parse_assertion(saml_response)
22 verify_signature(assertion, idp_cert)
23 # Never checks SubjectConfirmationData.Recipient
24 return assertion.subject.name_id
25 # Assertion intended for https://app-A.com/saml/acs
26 # is accepted at https://app-B.com/saml/acs
27
28# ❌ VULNERABLE: No InResponseTo check
29def process_saml_response(saml_response):
30 assertion = parse_assertion(saml_response)
31 verify_signature(assertion, idp_cert)
32 # Never checks InResponseTo matches a request we sent
33 return assertion.subject.name_id
34 # Attacker can use IdP-initiated flow to inject assertions
35 # even when only SP-initiated SSO should be allowed
36
37# ❌ VULNERABLE: No replay protection
38def process_saml_response(saml_response):
39 assertion = parse_assertion(saml_response)
40 verify_signature(assertion, idp_cert)
41 check_conditions(assertion) # Checks time bounds
42 # But no Response ID / Assertion ID tracking
43 return assertion.subject.name_id
44 # Same valid assertion can be submitted multiple times
45 # within its validity windowSAML NameID Comment Injection (CVE-2017-11427 and variants)
1// The NameID Comment Injection is one of the most elegant
2// SAML attacks discovered (affected ruby-saml, python-saml,
3// omniauth-saml, and many other libraries)
4
5// The IdP signs the following NameID:
6// <saml:NameID>user@corp.com</saml:NameID>
7
8// The attacker modifies it to:
9// <saml:NameID>user@corp.com<!--COMMENT-->.evil.com</saml:NameID>
10
11// Different XML parsers handle this differently:
12
13// Signature verification library (e.g., xmldsig):
14// Canonicalizes XML → strips comments → sees "user@corp.com.evil.com"
15// Digest matches original? NO — this particular variant is detected.
16
17// BUT some libraries use different canonicalization:
18// <saml:NameID>admin@corp.com<!---->user@corp.com</saml:NameID>
19
20// C14N (Canonical XML WITHOUT comments — most common):
21// Sees: "admin@corp.comuser@corp.com" (comments stripped)
22// Application code (some XML parsers return first text node):
23// Sees: "admin@corp.com" (only first text node!)
24
25// The actual CVE-2017-11427 attack:
26// <saml:NameID>admin@corp.com<!---->.user@corp.com</saml:NameID>
27// Signature verifies: "admin@corp.com.user@corp.com" (valid for user)
28// Application reads: "admin@corp.com" (FIRST text node only!)
29// Result: attacker authenticates as admin@corp.comAssertion Validation Checklist
| Property | Check | Attack if Missing |
|---|---|---|
| Audience | Conditions/AudienceRestriction matches our SP entity ID | Cross-SP assertion replay |
| NotBefore / NotOnOrAfter | Current time is within validity window | Indefinite assertion replay |
| Recipient | SubjectConfirmationData.Recipient matches our ACS URL | Cross-endpoint assertion replay |
| InResponseTo | Matches a request ID we recently issued | IdP-initiated injection bypass |
| Destination | Response.Destination matches our ACS URL | Response misdirection |
| Issuer | Matches expected IdP entity ID | Rogue IdP assertion injection |
| Response/Assertion ID | Not seen before (replay cache) | Assertion replay within validity window |
| Signature scope | Signature covers the assertion being used | XML Signature Wrapping (XSW) |
A SAML SP validates the signature and checks NotOnOrAfter, but doesn't check the Audience restriction. The developer says 'We only trust one IdP, so audience doesn't matter.' Is this safe?
4. Common Implementation Flaws
SAML implementation bugs go beyond assertion validation. Insecure XML parsing, certificate handling, and protocol configuration all create exploitable vulnerabilities.
Insecure XML parsing in SAML implementations
1# ❌ VULNERABLE: XML External Entity (XXE) in SAML parsing
2import xml.etree.ElementTree as ET
3
4def parse_saml_response(xml_string):
5 # ElementTree is safe by default in Python 3, BUT...
6 root = ET.fromstring(xml_string)
7 return root
8
9# ❌ VULNERABLE: lxml without disabling entities
10from lxml import etree
11
12def parse_saml_response(xml_string):
13 # lxml processes external entities by default!
14 parser = etree.XMLParser()
15 root = etree.fromstring(xml_string, parser)
16 return root
17 # Attacker sends SAML Response with XXE:
18 # <!DOCTYPE foo [
19 # <!ENTITY xxe SYSTEM "file:///etc/passwd">
20 # ]>
21 # <samlp:Response>
22 # <saml:NameID>&xxe;</saml:NameID>
23 # </samlp:Response>
24
25# ✅ SECURE: Disable all external entity processing
26from lxml import etree
27from defusedxml.lxml import fromstring # Use defusedxml!
28
29def parse_saml_response(xml_string):
30 return fromstring(xml_string)
31
32# ❌ VULNERABLE: XSLT injection via SAML
33# Some SAML libraries process XSLT transforms in signatures
34# Attacker injects an XSLT transform that executes code:
35# <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116">
36# <xsl:stylesheet>
37# <xsl:template match="/">
38# <!-- Arbitrary code execution via XSLT -->
39# </xsl:template>
40# </xsl:stylesheet>
41# </ds:Transform>Certificate and signature verification pitfalls
1// ❌ VULNERABLE: Accepting any certificate in the SAML response
2function verifySamlSignature(samlResponse) {
3 const cert = extractCertFromResponse(samlResponse);
4 // Using the certificate FROM the response to verify
5 // the signature IN the response — circular trust!
6 return xmlCrypto.verify(samlResponse, cert);
7 // Attacker generates their own key pair, signs a forged
8 // assertion, and embeds their public cert in the response
9}
10
11// ✅ SECURE: Use pre-configured IdP certificate
12function verifySamlSignature(samlResponse, trustedIdpCert) {
13 return xmlCrypto.verify(samlResponse, trustedIdpCert);
14}
15
16// ❌ VULNERABLE: Not verifying signature at all when missing
17function processSamlResponse(samlResponse) {
18 if (hasSignature(samlResponse)) {
19 verifySignature(samlResponse, idpCert);
20 }
21 // If no signature present, still processes the assertion!
22 return extractNameId(samlResponse);
23 // Attacker simply removes the signature element
24}
25
26// ❌ VULNERABLE: Only checking Response signature, not Assertion
27function processSamlResponse(samlResponse) {
28 verifySignature(samlResponse.response, idpCert);
29 // Verifies signature on <Response> element
30 // But doesn't check if the <Assertion> is also signed
31 // Attacker can inject unsigned assertions inside a signed response
32 return extractNameId(samlResponse);
33}
34
35// ❌ VULNERABLE: Weak signature algorithms
36function verifySamlSignature(samlResponse, idpCert) {
37 // Accepts any algorithm including SHA-1 or even MD5
38 return xmlCrypto.verify(samlResponse, idpCert);
39 // Should reject: SignatureMethod Algorithm="...rsa-sha1"
40 // Should require: SignatureMethod Algorithm="...rsa-sha256"
41}Real-world ruby-saml vulnerabilities
1# ruby-saml has had multiple critical CVEs
2# These patterns show what went wrong:
3
4# CVE-2017-11427: Comment Injection
5# ruby-saml used REXML which returned only the first text node
6# of an element, ignoring text after XML comments
7#
8# NameID: "admin@corp.com<!-- -->.legit-user@corp.com"
9# REXML text content: "admin@corp.com"
10# Signature digest: computed over full canonical text
11# Result: authenticate as admin@corp.com
12
13# CVE-2024-45409: Signature Wrapping
14# ruby-saml did not properly verify that the signature
15# covered the assertion being used for authentication
16# Allowed XSW attacks where a signed assertion was moved
17# and an unsigned forged assertion was processed instead
18
19# CVE-2016-5697: Signature Bypass via Missing Validation
20# ruby-saml allowed responses without signatures to be
21# processed when allow_sso_without_saml_signature was
22# inadvertently left enabled
23
24# ✅ SECURE: Current best practice with ruby-saml
25settings = OneLogin::RubySaml::Settings.new
26settings.idp_cert = File.read('idp_certificate.pem')
27settings.security[:want_assertions_signed] = true
28settings.security[:want_responses_signed] = true
29settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
30settings.security[:digest_method] = XMLSecurity::Document::SHA256
31settings.assertion_consumer_service_url = 'https://app.com/saml/acs'
32settings.sp_entity_id = 'https://app.com'The Certificate-in-Response Anti-Pattern
One of the most dangerous SAML mistakes is extracting the verification certificate from the SAML Response itself (from <ds:X509Certificate>) instead of using a pre-configured IdP certificate. This creates circular trust: the attacker generates their own key pair, signs a forged assertion with their private key, and includes their public certificate in the response. The signature verifies — because you used the attacker's certificate. Always validate signatures against a pre-registered, trusted IdP certificate stored in your SP configuration.
A SAML SP library checks for a signature and, if present, validates it. If no signature element exists in the response, the assertion is still accepted. The developer says 'the IdP always signs responses, so this is fine.' What's wrong?
5. Detection During Code Review
Reviewing SAML implementations during code review requires examining the full chain: XML parsing, signature verification, assertion extraction, and session creation. The following search patterns and checkpoints help identify vulnerabilities systematically.
Grep patterns for SAML vulnerability detection
1# Find SAML response processing entry points
2rg "saml|SAML" --type py --type js --type ts --type rb --type java -l
3rg "(acs|assertion_consumer|saml_callback|saml_response)" -l
4
5# Find XML parsing that may be vulnerable to XXE
6rg "XMLParser|etree\.parse|etree\.fromstring|DocumentBuilder" --type py --type java
7rg "parseXml|DOMParser|xml2js" --type js --type ts
8
9# Find certificate handling
10rg "(x509|certificate|idp_cert|idpCert)" --type py --type js --type rb
11rg "X509Certificate|KeyInfo" --type java
12
13# Find signature verification
14rg "(verify_signature|validateSignature|checkSignature)" -l
15rg "(xmldsig|xml-crypto|signedXml)" --type js --type ts
16
17# Find assertion extraction (potential XSW target)
18rg "(NameID|nameID|name_id|getAttribute|getAssertion)" --type py --type js --type rb
19rg "//.*Assertion|xpath.*Assertion" --type py --type js --type rb
20
21# Find missing validation
22rg "(audience|Audience|AudienceRestriction)" --type py --type js --type rb
23rg "(NotBefore|NotOnOrAfter|not_on_or_after)" --type py --type js --type rb
24rg "(InResponseTo|in_response_to|Recipient)" --type py --type js --type rbCode Review Decision Matrix for SAML
| What to Check | Red Flag | Secure Pattern |
|---|---|---|
| XML parsing | Default parser, no XXE protection | defusedxml, disabled entities, no XSLT |
| Certificate source | Extracted from response XML | Pre-configured IdP cert in SP config |
| Signature presence | Optional signature (if present, verify) | Reject if signature missing |
| Signature scope | Only Response OR only Assertion signed | Both signed, or at minimum the Assertion |
| Assertion extraction | XPath //Assertion or first child | Extract from the exact signed element |
| Audience check | Missing or commented out | Strict match against SP entity ID |
| Time validation | Missing NotBefore/NotOnOrAfter check | Checked with reasonable clock skew (< 5 min) |
| Replay protection | No assertion ID tracking | Cache of seen IDs within validity window |
| Destination check | Missing Response.Destination check | Matches configured ACS URL |
| Algorithm strength | Accepts SHA-1, RSA-1024 | Requires SHA-256+, RSA-2048+ |
- Trace the full assertion lifecycle — Follow the SAML Response from HTTP input to session creation. Every transformation (Base64 decode → XML parse → signature verify → assertion extract → user lookup → session create) is an attack point.
- Check what the signature actually covers — Does the Reference URI in the Signature point to the Response, the Assertion, or both? If only the Response is signed, the Assertion content could be modified.
- Verify assertion extraction matches signature scope — If the signature covers element with ID="_abc", the SP must extract identity from that exact element — not from "the first Assertion" or an XPath query.
- Look for conditional signature verification — Code like
if (hasSignature) { verify() }means unsigned responses are accepted. Always require signatures. - Check for XML comment handling in NameID — If the XML parser returns only the first text node of a NameID element, comment injection attacks may be possible.
- Review clock skew tolerance — A tolerance of more than 5 minutes for NotBefore/NotOnOrAfter creates a wider replay window. Some implementations use 10+ minutes or skip the check entirely.
During code review, you find the SP extracts the IdP certificate from <ds:X509Certificate> in the SAML Response, then uses it to verify the response signature. What do you flag?
6. Prevention Strategies
Securing SAML implementations requires defense in depth: use a well-maintained library, configure it strictly, validate every assertion property, and protect the XML parsing layer.
Secure SAML validation in Python (python3-saml)
1# ✅ SECURE: Comprehensive SAML validation with python3-saml
2from onelogin.saml2.auth import OneLogin_Saml2_Auth
3
4def process_saml_response(request):
5 saml_settings = {
6 "strict": True, # CRITICAL: enables all validations
7 "sp": {
8 "entityId": "https://app.example.com",
9 "assertionConsumerService": {
10 "url": "https://app.example.com/saml/acs",
11 "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
12 },
13 },
14 "idp": {
15 "entityId": "https://idp.corp.com",
16 "singleSignOnService": {
17 "url": "https://idp.corp.com/sso",
18 "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
19 },
20 "x509cert": IDP_CERTIFICATE, # Pre-registered, NOT from response
21 },
22 "security": {
23 "wantAssertionsSigned": True,
24 "wantMessagesSigned": True,
25 "wantNameIdEncrypted": False,
26 "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
27 "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256",
28 "rejectDeprecatedAlgorithm": True,
29 },
30 }
31
32 auth = OneLogin_Saml2_Auth(request, saml_settings)
33 auth.process_response()
34
35 errors = auth.get_errors()
36 if errors:
37 raise SecurityError(f"SAML validation failed: {errors}")
38
39 if not auth.is_authenticated():
40 raise SecurityError("SAML authentication failed")
41
42 name_id = auth.get_nameid()
43 attributes = auth.get_attributes()
44 session_index = auth.get_session_index()
45
46 return name_id, attributes, session_indexSecure SAML validation in Node.js (passport-saml / @node-saml/node-saml)
1// ✅ SECURE: Strict configuration with passport-saml
2const samlConfig = {
3 entryPoint: 'https://idp.corp.com/sso',
4 issuer: 'https://app.example.com',
5
6 // Pre-registered IdP certificate — NOT extracted from response
7 cert: fs.readFileSync('./idp-certificate.pem', 'utf-8'),
8
9 // Require signed assertions and responses
10 wantAssertionsSigned: true,
11 wantAuthnResponseSigned: true,
12
13 // Validate audience
14 audience: 'https://app.example.com',
15
16 // Validate recipient / destination
17 callbackUrl: 'https://app.example.com/saml/acs',
18
19 // Reject old/weak algorithms
20 signatureAlgorithm: 'sha256',
21 digestAlgorithm: 'sha256',
22
23 // Set reasonable clock skew tolerance
24 acceptedClockSkewMs: 180000, // 3 minutes max
25
26 // Validate InResponseTo (requires request tracking)
27 validateInResponseTo: 'always',
28
29 // Disable passive SSO to prevent unwanted assertions
30 disableRequestedAuthnContext: false,
31};
32
33// ✅ SECURE: Adding replay protection
34const processedAssertionIds = new Set();
35
36function checkReplay(assertionId) {
37 if (processedAssertionIds.has(assertionId)) {
38 throw new Error('Assertion replay detected');
39 }
40 processedAssertionIds.add(assertionId);
41 // Clean up expired IDs periodically
42}Secure SAML validation in Java (OpenSAML)
1// ✅ SECURE: OpenSAML-based SAML validation
2public class SecureSamlValidator {
3
4 private final X509Certificate trustedIdpCert;
5 private final String expectedAudience;
6 private final String expectedAcsUrl;
7 private final Set<String> processedAssertionIds = ConcurrentHashMap.newKeySet();
8
9 public SamlAuthResult validate(Response samlResponse) throws SamlException {
10 // 1. Verify destination
11 if (!expectedAcsUrl.equals(samlResponse.getDestination())) {
12 throw new SamlException("Invalid Destination");
13 }
14
15 // 2. Verify signature using TRUSTED certificate (not from response)
16 SignatureValidator.validate(
17 samlResponse.getSignature(), trustedIdpCert);
18
19 // 3. Get the signed assertion — verify it's the one the signature covers
20 Assertion assertion = getSignedAssertion(samlResponse);
21
22 // 4. Verify assertion signature too
23 if (assertion.getSignature() != null) {
24 SignatureValidator.validate(
25 assertion.getSignature(), trustedIdpCert);
26 }
27
28 // 5. Validate conditions
29 Conditions conditions = assertion.getConditions();
30 Instant now = Instant.now();
31
32 if (now.isBefore(conditions.getNotBefore().minus(Duration.ofMinutes(3)))) {
33 throw new SamlException("Assertion not yet valid");
34 }
35 if (now.isAfter(conditions.getNotOnOrAfter().plus(Duration.ofMinutes(3)))) {
36 throw new SamlException("Assertion expired");
37 }
38
39 // 6. Validate audience
40 boolean audienceValid = conditions.getAudienceRestrictions().stream()
41 .flatMap(ar -> ar.getAudiences().stream())
42 .anyMatch(a -> expectedAudience.equals(a.getURI()));
43 if (!audienceValid) {
44 throw new SamlException("Invalid audience");
45 }
46
47 // 7. Check replay
48 String assertionId = assertion.getID();
49 if (!processedAssertionIds.add(assertionId)) {
50 throw new SamlException("Assertion replay detected");
51 }
52
53 // 8. Validate SubjectConfirmation
54 validateSubjectConfirmation(assertion);
55
56 return new SamlAuthResult(assertion.getSubject().getNameID().getValue());
57 }
58}SAML Library Security Comparison
| Library | Language | XSW Protection | Key Considerations |
|---|---|---|---|
| python3-saml (OneLogin) | Python | Yes (strict mode) | Set strict=True; rejectDeprecatedAlgorithm=True |
| passport-saml / @node-saml | Node.js | Yes (recent versions) | Set wantAssertionsSigned; validateInResponseTo=always |
| ruby-saml | Ruby | Yes (post CVE-2024-45409) | Update to latest; enable all security settings |
| OpenSAML | Java | Partial (manual checks needed) | Verify signature scope manually; use trusted cert only |
| Spring Security SAML | Java | Yes | Use OpenSAML 4.x+ backend; configure strict validation |
| ComponentSpace | C#/.NET | Yes | Enable assertion signature verification |
Defense in Depth: SAML Metadata
Automate IdP certificate trust via SAML Metadata exchange rather than manual certificate configuration. The SP downloads the IdP's metadata document (which contains the signing certificate) from a trusted, HTTPS URL and caches it. When the IdP rotates certificates, the SP picks up the new cert from metadata. This reduces the risk of misconfigured or expired certificates. However, the initial metadata URL must be configured securely — an attacker who controls the metadata URL controls the trusted certificate.
Which of the following is the MOST critical configuration setting when setting up a SAML SP library?