Troubleshoot SAML assertion rejection: signature, clock skew, NameID
For developers debugging SAML logins that fail at the service provider with signature validation errors, time drift, or NameID mismatches. This runbook gives you a fast decision path, exact commands, and concrete fixes you can apply in your IdP/SP config and infrastructure.
TL;DR — When a service provider rejects a SAML assertion, the fastest wins are: compare the SP’s stored IdP signing certificate to the certificate actually present in the SAML response, check both IdP and SP clocks for drift beyond a couple of minutes, and confirm the NameID format/value matches what the SP expects. In practice, certificate rollover and NTP drift cause most incidents; NameID mismatches are the next most common after SSO config changes. Reading time: ~6 min
The scenario
You rotate an IdP signing certificate on a Tuesday afternoon, the change ticket closes cleanly, and five minutes later users start reporting that SSO “just spins” and then drops them back on the login page. Your app is up, health checks are green, and direct local admin login still works. The SP logs show generic SAML failures, but not enough to tell whether this is a bad signature, expired assertion window, or the wrong user identifier being sent. Meanwhile, your customer is forwarding screenshots of “Unable to sign in” from three different browsers.
Symptoms
- Browser gets redirected back to the SP login page after IdP authentication completes.
- SP returns
HTTP 400,401, or403on the Assertion Consumer Service (ACS) endpoint. - SP logs contain messages like:
SAML response rejected: signature validation failed
Invalid signature on SAML assertion
Signature verification failed for issuer https://idp.example.com/metadata
No trusted signing certificate found
Assertion is not yet valid
Assertion has expired
Current time is outside NotBefore/NotOnOrAfter conditions
Subject NameID does not match any user
Unsupported NameID format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
Expected NameID format urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
Audience restriction validation failed
Recipient mismatch
- User-facing errors are usually vague:
We couldn't sign you in.
Single sign-on failed.
Authentication failed. Contact your administrator.
- If the ACS endpoint is wrong or redirected,
curl -Ishows it immediately:
curl -I https://sp.example.com/saml/acs
HTTP/2 302
location: /login
content-type: text/html; charset=utf-8
That redirect is often enough to break SAML POST handling.
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| IdP signing certificate in the SP is stale after cert rollover | Very common | `xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -v "//ds:X509Certificate" saml-response.xml |
| Clock skew between SP/IdP and assertion validity window too tight | Very common | `date -u && chronyc tracking 2>/dev/null |
| NameID format or value no longer matches SP user mapping | Common | xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -v "//s:Subject/s:NameID/@Format" -o " " -v "//s:Subject/s:NameID" saml-response.xml |
| ACS URL / Recipient / Audience mismatch after URL or proxy change | Common | xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -v "//s:Audience" -o "\n" -v "//s:SubjectConfirmationData/@Recipient" saml-response.xml |
| Assertion or response is signed differently than the SP expects | Occasional | xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//*[local-name()='Response' or local-name()='Assertion']" -v "local-name()" -o ":" -v "count(.//ds:Signature)" -n saml-response.xml |
Step-by-step diagnosis
- Capture and decode one failing SAML response
- In your browser dev tools, copy the
SAMLResponseform field from the POST to the ACS endpoint, then decode it locally:
- In your browser dev tools, copy the
echo 'PASTE_BASE64_SAMLRESPONSE_HERE' | base64 -d > saml-response.xml
xmllint --format saml-response.xml | sed -n '1,80p'
- If
base64: invalid input, you copied a URL-encoded value. Decode first:
python3 - <<'PY'
import sys, urllib.parse, base64
s=sys.stdin.read().strip()
print(base64.b64decode(urllib.parse.unquote_plus(s)).decode())
PY
- If you cannot produce valid XML, stop and fix your capture method before changing config.
- Check the SP error log for the exact rejection reason
- Search for the request timestamp around the failed login:
grep -Ei 'saml|assertion|signature|nameid|audience|recipient|notbefore|notonorafter' /var/log/*/* 2>/dev/null | tail -50
- If you see
signature validation failedorNo trusted signing certificate found, jump to Fixes → IdP signing certificate in the SP is stale. - If you see
Assertion is not yet valid,expired, oroutside NotBefore/NotOnOrAfter, jump to Fixes → Clock skew. - If you see
NameIDoruser not found, jump to Fixes → NameID format or value mismatch. - If you see
Audience,Recipient, or ACS mismatch, jump to Fixes → ACS URL / Recipient / Audience mismatch.
- Inspect the assertion timestamps and compare to host time
xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -m "//s:Conditions" -v @NotBefore -o " " -v @NotOnOrAfter -n saml-response.xml
date -u
chronyc tracking 2>/dev/null || timedatectl status
- This is your problem if current UTC time is outside the assertion window, or if
chronycshows large offset, for example:
Reference ID : 169.254.169.123
System time : 0.842391234 seconds fast of NTP time
Last offset : +0.812345678 seconds
- Offsets under ~1 second are usually fine. Failures commonly start when drift exceeds the SP’s allowed skew, often 120-300 seconds.
- If bad, jump to Fixes → Clock skew.
- Extract NameID format and value
xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -v "//s:Subject/s:NameID/@Format" -o "\n" -v "//s:Subject/s:NameID" -n saml-response.xml
- This is your problem if the SP expects email but receives an opaque persistent ID, or vice versa. Typical mismatch:
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
8f5c1d0d-4b8d-4f2f-a4b8-9d2f6e9d1c11
- If your app maps users by email and receives a persistent identifier with no matching lookup attribute, jump to Fixes → NameID format or value mismatch.
- Compare the signing certificate in the response with the SP’s configured IdP certificate
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -v "(//ds:X509Certificate)[1]" -n saml-response.xml | fold -w 64 > idp-cert-from-response.b64
{ echo '-----BEGIN CERTIFICATE-----'; cat idp-cert-from-response.b64; echo '-----END CERTIFICATE-----'; } > idp-cert-from-response.pem
openssl x509 -in idp-cert-from-response.pem -noout -subject -issuer -fingerprint -sha256 -dates
- Compare that fingerprint to the certificate currently configured in the SP.
- This is your problem if the fingerprints differ or the cert is expired.
- Jump to Fixes → IdP signing certificate in the SP is stale.
- Check Recipient, Audience, and ACS behavior
xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -v "//s:Audience" -o "\n" -v "//*[local-name()='SubjectConfirmationData']/@Recipient" -n saml-response.xml
curl -I https://sp.example.com/saml/acs
- This is your problem if
Audiencedoes not equal the SP entity ID,Recipientis an old URL, orcurl -Ishows a redirect or non-200/405 behavior on the ACS endpoint. - Jump to Fixes → ACS URL / Recipient / Audience mismatch.
- Check whether the SP expects response-signed vs assertion-signed documents
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//*[local-name()='Response' or local-name()='Assertion']" -v "local-name()" -o ":" -v "count(./ds:Signature)" -n saml-response.xml
- Example output:
Response:0
Assertion:1
- If your SP only trusts response signatures but the IdP signs only the assertion, or the reverse, jump to Fixes → Assertion or response is signed differently than the SP expects.
Fixes
IdP signing certificate in the SP is stale after cert rollover
Update the IdP metadata or paste the new signing certificate into the SP’s SAML configuration.
- If your SP supports metadata URL refresh, re-import from the IdP metadata URL in your provider dashboard, typically under
SSOorAuthenticationsettings. - If you manage XML metadata locally, fetch and inspect it:
curl -fsSL https://idp.example.com/metadata -o idp-metadata.xml
xmlstarlet sel -N md="urn:oasis:names:tc:SAML:2.0:metadata" -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']//ds:X509Certificate" -v . -n idp-metadata.xml | head -1 | fold -w 64 > signing-cert.b64
{ echo '-----BEGIN CERTIFICATE-----'; cat signing-cert.b64; echo '-----END CERTIFICATE-----'; } > signing-cert.pem
openssl x509 -in signing-cert.pem -noout -fingerprint -sha256 -dates -subject
- Paste
signing-cert.peminto the SP config or update the SP’s metadata store. - Trade-off: metadata URL auto-refresh reduces rollover incidents but can surprise you during emergency IdP changes. Pinning a cert is stable until the next rollover.
Verify it worked:
openssl x509 -in signing-cert.pem -noout -fingerprint -sha256
The fingerprint should match the certificate extracted from a fresh successful SAML response.
Clock skew between SP/IdP and assertion validity window too tight
Fix NTP on the SP first; if you control the IdP, check it too. Then widen allowed skew only if your SP supports it and your security policy accepts it.
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd 2>/dev/null || true
chronyc makestep 2>/dev/null || sudo systemctl restart chronyd 2>/dev/null || true
chronyc tracking 2>/dev/null || timedatectl status
- If you run in containers, check the host clock; containers inherit host time.
- If the SP has a configurable skew window, set it to 180-300 seconds rather than disabling time checks. Exact setting names vary by library/vendor.
- Trade-off: larger skew windows reduce false negatives but increase replay tolerance.
Verify it worked:
date -u && chronyc tracking 2>/dev/null | grep -E 'System time|Last offset'
Offsets should be small and new assertions should fall within NotBefore/NotOnOrAfter.
NameID format or value no longer matches SP user mapping
Change the IdP claim/release policy so NameID matches the identifier your SP uses, or reconfigure the SP to map users by a different attribute.
Common target values:
- Email login apps:
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - Stable opaque IDs:
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - Directory usernames:
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
If your SP can map from an attribute instead of NameID, prefer a stable attribute such as email or uid and keep NameID persistent.
Inspect available attributes in the assertion:
xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -m "//s:Attribute" -v @Name -o "=" -m "s:AttributeValue" -v . -o ";" -n saml-response.xml
- Update the IdP app config in your provider dashboard under the SAML attribute/claim mapping page.
- Update the SP mapping to the same field.
- Edge case: email renames break
emailAddressNameID if the SP keys accounts by previous email. Persistent IDs avoid that.
Verify it worked:
xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -v "//s:Subject/s:NameID/@Format" -o " " -v "//s:Subject/s:NameID" saml-response.xml
The format and value should match the SP’s configured user lookup.
ACS URL / Recipient / Audience mismatch after URL or proxy change
Update the IdP SAML app config with the current ACS URL and SP entity ID, and fix any reverse proxy redirect behavior.
Check for proxy issues first:
curl -I https://sp.example.com/saml/acs
Bad example:
HTTP/2 301
location: https://www.example.com/login
For nginx, preserve scheme/host so the app generates correct ACS URLs:
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app;
}
Then update the IdP app’s ACS/Reply URL and Audience/Entity ID to exactly match the SP values, including scheme and path.
⚠️ Changing ACS or entity ID on a production IdP app can break login for all users until both sides match. Apply during a maintenance window if you do not have a fallback admin login.
Verify it worked:
curl -I https://sp.example.com/saml/acs
The endpoint should no longer redirect away from the ACS path, and fresh assertions should show matching Recipient and Audience.
Assertion or response is signed differently than the SP expects
Align the IdP signing mode with the SP validator.
- If the SP expects signed assertions, enable assertion signing in the IdP.
- If the SP expects signed responses, enable response signing or relax the SP to accept assertion signatures if supported.
- If both are available, signing both is usually safest but can expose bugs in older libraries; test before rollout.
Re-check signature placement:
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//*[local-name()='Response' or local-name()='Assertion']" -v "local-name()" -o ":" -v "count(./ds:Signature)" -n saml-response.xml
Verify it worked: the signature appears where the SP expects it, and the SP log no longer reports signature placement/validation errors.
Prevention
- Monitor certificate expiry for IdP signing certs and alert 30/14/7 days before rollover:
openssl x509 -in signing-cert.pem -noout -enddate
Add this to a daily job and page if expiry is near.
- Pin a SAML smoke test in CI/staging that decodes a sample response and checks
Audience,Recipient, NameID format, and signature presence:
xmlstarlet sel -N s="urn:oasis:names:tc:SAML:2.0:assertion" -t -v "//s:Audience" -o "\n" -v "//s:Subject/s:NameID/@Format" sample-response.xml
Fail the pipeline if values drift from expected config.
- Alert on NTP drift on all SP hosts:
chronyc tracking | grep -E 'System time|Last offset'
Export offset to your monitoring system and alert above 60 seconds.
- Store SAML config as code: entity ID, ACS URL, expected NameID format, and cert fingerprints in version control:
{
"entity_id": "https://sp.example.com/saml/metadata",
"acs_url": "https://sp.example.com/saml/acs",
"expected_nameid_format": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"idp_signing_cert_sha256": "AB:CD:..."
}
Review diffs in pull requests before changing SSO behavior.
-
Keep a break-glass local admin account outside SAML so you can still access the SP when SSO is broken. Test it quarterly.
-
Log the exact SAML validation reason at the SP, not just
SSO failed. If you own the app, log the validator exception string and request ID so support can correlate one failed login to one assertion.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI