In this article
July 20, 2026
July 20, 2026

SAML security best practices for SaaS developers

How to validate assertions correctly, manage certificates, prevent replay attacks, and avoid the misconfigurations that turn SAML SSO into a liability.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

SAML SSO is one of the first things enterprise customers ask for, and one of the last things most SaaS developers spend time getting right from a security standpoint. The typical path is: get it working, ship it, move on. The problem is that SAML has a large attack surface, and most of the vulnerabilities that have made headlines in the past few years (across ruby-saml, authentik, OneUptime, Fortinet, and others) didn't come from flaws in the SAML specification itself. They came from service providers not validating assertions correctly.

This guide covers the most important security controls to have in place if your SaaS application accepts SAML assertions. It assumes you already have a working SAML integration and are asking the follow-up question: is it secure?

Validate every part of the assertion

The single most common source of SAML vulnerabilities is incomplete assertion validation. A SAML response contains multiple fields that must be checked, and skipping any one of them can create an authentication bypass.

Every SAML response your application receives should be validated against all of the following before a session is created:

  • Signature: the response or assertion must be signed by the expected IdP, using the certificate you have on file for that connection. Accepting unsigned assertions, or accepting signatures from certificates not associated with the connection, is an authentication bypass. Do not validate the signature on the response envelope alone and skip the assertion signature. Validate both if both are present.
  • Issuer: the <saml:Issuer> element must match the entity ID you have configured for the IdP. If it doesn't, the assertion came from an unexpected source.
  • Audience restriction: the <saml:AudienceRestriction> element must contain your application's entity ID. This ensures the assertion was issued for your application specifically and not for a different service provider. Skipping this check means an assertion intended for another SP could be used to log in to yours.
  • Destination: the Destination attribute on the response must match the Assertion Consumer Service (ACS) URL that received the response. This prevents an assertion delivered to one endpoint from being replayed at a different one.
  • Timestamps: the NotBefore and NotOnOrAfter conditions must be checked against the current time, with a small clock skew tolerance (typically no more than 5 minutes). Assertions outside their validity window should be rejected.
  • InResponseTo: for SP-initiated flows, the InResponseTo attribute on the response must match the ID of the AuthnRequest your application sent. If there's no match, the response is unsolicited and should be rejected unless you have explicitly opted into IdP-initiated SSO.

Checking all of these is not optional. Each one that gets skipped in the name of "making it work" is a potential bypass.

Use a well-maintained SAML library

Writing your own XML signature verification is one of the riskiest things you can do in a SAML integration. XML signature validation is subtle, and the bugs that create exploitable vulnerabilities (like XML Signature Wrapping, where the signed portion of the document doesn't match the portion the application reads) are nearly impossible to catch through code review alone.

Use a SAML library that is actively maintained, has a published security advisory history, and has been audited or widely deployed. Check when the library last received a security-related update. A library with no commits in two years is a liability, even if it appears to work correctly.

When a new CVE is published against a SAML library you depend on, treat it as urgent. The window between disclosure and active exploitation for authentication bypass vulnerabilities is often measured in days, not weeks.

Reject SHA-1 and other weak algorithms

SHA-1 is deprecated for cryptographic use and should not be accepted as a signing algorithm in SAML assertions. Some IdPs and older service providers still default to it or allow it as a fallback, but accepting SHA-1-signed assertions weakens the integrity guarantees of your SAML implementation.

Require RSA-SHA256 or RSA-SHA512 as the minimum acceptable signing algorithm. If your SAML library allows you to configure an allowlist of accepted algorithms, use it. Reject any assertion signed with an algorithm not on that list rather than falling back silently.

Similarly, if you support encrypted assertions, use AES-256 for assertion encryption. Older configurations may default to AES-128, which is still considered secure but worth auditing if your customers have compliance requirements specifying otherwise.

Implement replay attack prevention

A replay attack is when an attacker captures a valid SAML assertion and submits it a second time to gain unauthorized access. If your application doesn't track which assertion IDs it has already processed, the same assertion can be used more than once within its validity window.

Prevention is straightforward: store each assertion ID in a short-lived cache when you process it, and reject any incoming assertion whose ID is already in that cache. The cache entry only needs to live as long as the assertion's NotOnOrAfter window, typically a few minutes.

The implementation needs to be shared across all instances of your application if you run multiple replicas. A per-process in-memory store won't work in a load-balanced environment because a replayed assertion could arrive at a different instance than the original. Use a shared store like Redis or your database.

  
async function isReplayedAssertion(assertionId, notOnOrAfter) {
  const key = `saml:assertion:${assertionId}`;
  const existing = await redis.get(key);

  if (existing) {
    return true; // already seen — reject
  }

  const ttl = Math.ceil((new Date(notOnOrAfter) - Date.now()) / 1000);
  await redis.set(key, '1', { EX: ttl });

  return false;
}
  

Also store and validate request IDs for SP-initiated flows. When you send an AuthnRequest, store its ID and check that the response's InResponseTo matches a known outstanding request. Clear the stored ID once the response is processed so it can't be reused.

Manage certificates carefully

SAML connections are anchored by X.509 certificates. The IdP signs assertions with its private key, and your application verifies those signatures using the IdP's public certificate. If that certificate is wrong, expired, or has been rotated without your application being updated, SSO breaks. If you're trusting the wrong certificate, you may accept forged assertions.

A few rules for certificate management:

  • Pin the certificate to the connection, not globally. Each SSO connection should have its own trusted certificate. Trusting the same certificate across all connections means a compromised or misconfigured IdP for one customer can sign assertions accepted by others.
  • Support certificate rotation without downtime. IdPs rotate their signing certificates periodically, and this shouldn't require a manual update on your side. The safest approach is to trust all certificates present in the IdP's current metadata, not just the one you configured at setup time. Most IdPs publish their metadata at a well-known URL and include both the active and next certificate during rotation windows.
  • Alert on certificate expiry. Track when each connection's certificate expires and alert in advance, not when SSO is already broken. A reasonable lead time is 30 days.
  • Refresh metadata automatically. Rather than storing a static certificate, periodically fetch the IdP's metadata URL and update the trusted certificate set. This handles rotation transparently and avoids the class of outage where SSO breaks because a certificate was rotated and nobody noticed.

Be deliberate about IdP-initiated SSO

SP-initiated SSO is the default flow: the user starts at your application, gets redirected to the IdP to authenticate, and returns with an assertion. The assertion carries an InResponseTo value that matches the request your application sent, which gives you a way to verify that the assertion was issued in response to a specific, known request.

IdP-initiated SSO inverts this. The user starts at the IdP (a company portal, for example) and is sent directly to your application with an unsolicited assertion. Because there's no prior request, there's no InResponseTo to check, and your application loses the ability to detect assertion replay and injection using that binding.

This doesn't mean IdP-initiated SSO should never be supported. Some enterprise customers require it for their internal portals. But it should be an explicit decision, not a default. If you support it, implement compensating controls: strict assertion lifetime enforcement, replay detection by assertion ID, and tight audience and destination validation. And make it configurable per connection rather than enabling it globally.

If you don't have customers who require it, disable it.

Scope group and role claims tightly

Attributes included in SAML assertions, particularly group membership and role claims, have security implications beyond what's visible during testing. A few things to watch for:

  • Bloated group assertions. If the IdP sends all of a user's directory groups rather than a filtered subset, assertions can become large enough to trigger HTTP header size limits, causing silent failures. More importantly, it leaks your customer's internal group structure to your logs and your application. Configure IdP-side filters to send only the groups relevant to your application.
  • Unvalidated role escalation. If your application uses a SAML attribute to determine a user's role or permission level, make sure that attribute can only be set by the IdP and cannot be influenced by user input. Also ensure your role mapping logic has a defined behavior for unexpected values: either reject the assertion or assign a minimal default role, rather than assuming the user has no special access.
  • Attribute spoofing in multi-tenant configurations. If multiple customers share an application instance and you use SAML attributes to scope data access per tenant, verify that the attribute values in an assertion can only have been set by the expected IdP. An assertion from a misconfigured or compromised IdP for one tenant should not be able to claim attribute values that grant access to another tenant's data.

Keep assertion lifetimes short

The NotOnOrAfter timestamp on a SAML assertion defines how long it remains valid. A longer window gives an attacker more time to reuse a captured assertion before it expires. A shorter window reduces that exposure.

Most IdPs default to assertion lifetimes of 5 minutes or less, but some enterprise configurations extend this significantly. If you have control over the IdP configuration (for example, through your own WorkOS-managed SSO), keep assertion lifetimes to 5 minutes or shorter. If you don't control the IdP, you can enforce a maximum lifetime on your side: reject any assertion whose validity window is longer than your policy allows.

Short lifetimes work in combination with replay prevention, not as a replacement for it. Even a 1-minute assertion can be replayed many times within that window if you're not tracking assertion IDs.

Log SAML validation failures

Silent failures are the most dangerous kind in authentication systems. If an assertion fails validation for any reason, the failure should be logged with enough detail to diagnose what went wrong without exposing the assertion contents.

Useful fields to log on validation failure:

  • Timestamp
  • Connection or tenant ID
  • Failure reason (signature validation failed, audience mismatch, assertion expired, etc.)
  • Assertion ID, if present
  • IdP entity ID
  • ACS URL that received the response

Do not log the full assertion or the raw SAML response, as these can contain sensitive user attributes. Do not log the user's password or any credential material (SAML doesn't involve passwords on the SP side, but be careful if you're logging raw request bodies).

Alert on elevated failure rates. A spike in signature validation failures or audience mismatch errors can be an early signal of misconfiguration, certificate rotation problems, or an active attack.

Test your validation logic explicitly

The most important thing you can do after implementing SAML security controls is verify that they actually reject what they're supposed to reject, not just that they accept valid assertions.

Write negative test cases for each validation check:

  • An assertion with no signature should be rejected
  • An assertion signed with an untrusted certificate should be rejected
  • An assertion with a mismatched audience should be rejected
  • An assertion with an expired NotOnOrAfter should be rejected
  • An assertion with a replayed assertion ID should be rejected
  • An assertion with an InResponseTo that doesn't match any outstanding request should be rejected for SP-initiated flows

If any of these pass when they shouldn't, you have a gap in your validation logic. SAML security bugs are almost always about what gets accepted, not what gets rejected.

How WorkOS handles SAML security

If you're using WorkOS for SSO, assertion validation, certificate management, and replay protection are handled by the WorkOS platform rather than your application code. WorkOS validates signatures, audience restrictions, timestamps, and assertion IDs on every incoming SAML response before surfacing the user profile to your application through the SDK.

Certificate rotation is handled automatically: WorkOS refreshes IdP metadata on a regular schedule and updates trusted certificates without requiring manual intervention or causing authentication downtime.

For the security controls that live on the application side, such as session management, role mapping, and per-tenant data isolation, WorkOS provides the normalized user profile and raw IdP attributes your application needs to implement them correctly. See the WorkOS SSO documentation for details on what's handled by the platform and what your application is responsible for.

Key takeaways

SAML security failures almost always come from incomplete validation on the service provider side, not from fundamental weaknesses in the protocol. The controls here are not exotic, they are the baseline that every SAML integration should have in place.

The checklist version:

  • Validate signature, issuer, audience, destination, timestamps, and InResponseTo on every assertion
  • Use a well-maintained SAML library; treat CVEs as urgent
  • Reject SHA-1 and other deprecated signing algorithms
  • Implement replay prevention using a shared assertion ID store
  • Pin certificates per connection; support rotation without downtime; automate metadata refresh
  • Make IdP-initiated SSO an explicit opt-in, not a default
  • Filter group and role claims to what your application actually needs
  • Keep assertion lifetimes short, ideally 5 minutes or less
  • Log validation failures with enough detail to diagnose problems
  • Write negative test cases that verify invalid assertions are rejected