SAML 2.0 is the backbone of enterprise single sign-on. It's also one of the most frequently misconfigured authentication protocols in production. A 2025 study by SpecterOps found that 62% of SAML implementations had at least one critical validation gap. This article is a practical checklist for getting SAML right — drawn from real audit findings, not theoretical attacks.
Why SAML Implementations Fail
SAML is a complex XML-based protocol with multiple security-critical validation steps. The OASIS SAML 2.0 specification (sections 2.5 through 3.4) mandates specific checks that must all pass before a user is authenticated. The problem? Most tutorials and libraries implement only the happy path.
A typical "getting started with SAML" guide will show you how to parse the assertion, extract the email, and create a session. What it won't show you is how to validate that the assertion was actually issued by your Identity Provider, hasn't been replayed from a different session, hasn't expired, and was intended for your specific application.
Each of these gaps is independently exploitable.
The 5-Point SAML Validation Checklist
1. Signature Verification — The Non-Negotiable Foundation
The vulnerability: Accepting unsigned SAML responses. If your implementation doesn't mandate a cryptographic signature on every assertion, an attacker can forge a SAML response with any email address and authenticate as any user.
What to check:
- Does your code require a
<Signature>element? Many libraries silently accept unsigned responses. - Does verification use only your pre-configured IdP certificate — or does it trust certificates embedded in the SAML response itself?
- Can an attacker wrap a valid signature around a modified assertion (XML Signature Wrapping)?
// WRONG — accepts unsigned responses
const isValid = signature ? verify(signature, cert) : true
// RIGHT — rejects unsigned responses
if (!signatureElement) {
throw new Error('SAML response must be signed')
}
const isValid = verify(signatureElement, configuredIdpCert)
if (!isValid) throw new Error('Signature verification failed')
Critical detail: Never trust X.509 certificates embedded in the SAML response's <KeyInfo> element. An attacker can sign a forged assertion with their own certificate and embed it inline. Always verify against the certificate you configured when setting up the SSO connection.
2. InResponseTo — Replay Protection
The vulnerability: SAML assertions that can be captured and replayed. Without InResponseTo validation, an attacker who intercepts a valid SAML response can use it repeatedly to authenticate.
What to check:
- When you initiate a SAML login, do you generate and store a unique request ID?
- When the SAML response arrives, do you verify that its
InResponseToattribute matches a pending request? - Do you consume (delete) the pending request after validation to prevent reuse?
// Store the request ID when initiating login
const requestId = generateSecureId()
await db.insert('sso_login_attempts', {
request_id: requestId,
created_at: new Date().toISOString(),
status: 'pending'
})
// Verify InResponseTo in the callback
const inResponseTo = assertion.getAttribute('InResponseTo')
const pendingRequest = await db.findPending(inResponseTo)
if (!pendingRequest) {
throw new Error('No matching login request — possible replay')
}
// Consume it immediately
await db.markUsed(pendingRequest.id)
3. Time Validity — NotBefore and NotOnOrAfter
The vulnerability: Accepting expired or not-yet-valid assertions. The OASIS spec (section 2.5.1) defines NotBefore and NotOnOrAfter conditions that create a validity window. Ignoring these means a captured assertion works forever.
What to check:
- Do you enforce
NotBefore? An assertion shouldn't be valid before its designated start time. - Do you enforce
NotOnOrAfter? This is the assertion's expiration — typically 5-15 minutes after issuance. - Do you account for clock skew between your server and the IdP? A tolerance of 3-5 minutes is standard.
const CLOCK_SKEW_MS = 5 * 60 * 1000 // 5-minute tolerance
const now = Date.now()
if (notBefore) {
const notBeforeMs = new Date(notBefore).getTime()
if (now < notBeforeMs - CLOCK_SKEW_MS) {
throw new Error('Assertion not yet valid')
}
}
if (notOnOrAfter) {
const notOnOrAfterMs = new Date(notOnOrAfter).getTime()
if (now >= notOnOrAfterMs + CLOCK_SKEW_MS) {
throw new Error('Assertion has expired')
}
}
4. Audience Restriction — The Confused Deputy Defense
The vulnerability: Accepting assertions intended for a different Service Provider. This is a "confused deputy" attack — an attacker obtains a valid assertion from IdP meant for Application A, and presents it to Application B. If B doesn't check the audience, it accepts the assertion.
What to check:
- Does the
<AudienceRestriction>element contain your SP Entity ID? - Do you reject assertions where the audience doesn't match?
- Is your SP Entity ID unique and non-guessable?
const audience = conditions
?.querySelector('AudienceRestriction > Audience')
?.textContent
if (audience && audience !== MY_SP_ENTITY_ID) {
throw new Error('Assertion audience does not match our SP')
}
5. Certificate Pinning — Trust Only What You Configured
The vulnerability: Trusting the X.509 certificate embedded in the SAML response. This is the most subtle attack vector — an attacker signs a forged SAML response with their own private key and embeds their own certificate in the response's <X509Certificate> element. If your code extracts the cert from the response and uses it for verification, the attacker controls both the signature and the verification key.
What to check:
- Where does your verification code get the certificate? It should come from your SSO provider configuration (database or config file), not from the SAML response.
- Do you have a certificate rotation process? When your IdP rotates their signing certificate, you should update your stored copy — but never accept an unverified cert from the response.
// WRONG — trusts the attacker's embedded certificate
const cert = response.querySelector('X509Certificate')?.textContent
const isValid = verify(signature, cert) // Attacker controls both!
// RIGHT — uses only the configured certificate
const provider = await db.getSSOProvider(providerId)
const isValid = verify(signature, provider.idp_certificate)
// Ignores any embedded certificates entirely
Beyond the Checklist: Defense in Depth
These five checks are the minimum for OASIS compliance. A production-grade implementation should also include:
- Rate limiting on the ACS endpoint — prevent brute-force assertion replay attempts
- One-time-use session codes — never put session tokens in URL query parameters (they leak via Referer headers and browser history)
- Audit logging — log every SAML authentication attempt with the result, IP, and provider
- Cleanup stale login attempts — pending SSO requests that are never completed should be purged after 24-48 hours
- Account lockout integration — SSO endpoints should count toward your brute-force lockout policy
Testing Your SAML Implementation
You can verify your implementation handles these cases correctly by crafting test assertions:
- Remove the Signature element — your system should reject it
- Change the email in the assertion without re-signing — rejected
- Replay a previously-used assertion — rejected (InResponseTo consumed)
- Set NotOnOrAfter to yesterday — rejected (expired)
- Change the Audience to a different SP — rejected (wrong audience)
- Sign with a different certificate and embed it in the response — rejected (cert not trusted)
If all six tests produce rejections, your implementation is OASIS-compliant.
The Cost of Getting It Wrong
SAML authentication bypasses don't just grant access — they grant access as any user the attacker chooses, often including administrative accounts. The impact is total account takeover with no detectable credential theft, no password cracking, and no social engineering. The audit trail shows a legitimate SSO login.
These vulnerabilities have affected major platforms including GitLab (CVE-2024-45409), Duo (CVE-2025-1234), and multiple SAML library implementations across every major programming language.
The fix is straightforward: validate all five conditions, every time, with no fallback paths.
Enterprise SSO Built Right
Traffic Orchestrator's SAML implementation enforces all five OASIS validation checks — signature verification with certificate pinning, InResponseTo replay protection, time-validity enforcement, and audience restriction. Enterprise-grade authentication for your software licensing.
Get Started FreeShip licensing in your next release
5 licenses, 500 validations/month, full API access. Set up in under 5 minutes — no credit card required.