Troubleshoot SAML assertion rejection: signatures, clock skew, NameID
For developers debugging SSO failures where the service provider rejects a SAML assertion. This runbook gives you a fast decision path for the three common causes: bad signature validation, clock skew, and NameID mismatches, with concrete commands and config checks.
TL;DR — When a service provider rejects a SAML assertion, the fastest checks are: compare the IdP signing certificate actually used vs the SP-trusted certificate, verify both sides' clocks are within a couple of minutes, and confirm the NameID format/value matches what the SP expects. The most common fix is updating the SP with the current IdP signing cert after a rotation, then re-testing with a captured assertion. Reading time: ~6 min
The scenario
It is Tuesday afternoon, you rotate an IdP certificate or change a SAML attribute mapping, and suddenly users can still hit the login page but bounce back with a generic "Sign-in failed" message. Your app logs show the SAML response reached the ACS endpoint, but the service provider rejects the assertion before session creation. Product is asking whether this is "just a user issue," while your test account fails the same way in an incognito window. You need to determine quickly whether the break is signature validation, clock skew, or the wrong NameID.
Symptoms
- Browser lands on an error page after SSO redirect loop, often with HTTP
400or401from the ACS endpoint. - SP logs contain messages like:
SAML response rejected: signature validation failed
InvalidSignature: Signature verification failed for assertion
No trusted signing certificate found for issuer https://idp.example.com/metadata
- Or time-related failures:
Assertion is not yet valid
Conditions is not valid due to clock skew
Current time is on or after NotOnOrAfter
SubjectConfirmationData NotBefore validation failed
- Or identity mapping failures:
NameID is missing
Unsupported NameID format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
User not found for NameID 'alice@example.com'
Expected persistent NameID, got transient
- User sees one of:
- "We couldn't sign you in"
- "Authentication failed"
- repeated redirects between IdP and SP, then a final error page
- If the ACS endpoint is wrong or behind a proxy issue,
curl -Imay show an unexpected redirect shape:
$ curl -I https://sp.example.com/saml/acs
HTTP/2 302
location: /login
cache-control: no-store
That is not itself a SAML validation error, but it often appears alongside bad ACS or relay state config.
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| IdP signing certificate at the SP is stale or wrong after rotation | Very common | xmlsec1 --verify --pubkey-cert-pem idp-signing.crt assertion.xml |
Clock skew between IdP, SP, or reverse proxy host breaks NotBefore/NotOnOrAfter | Common | date -u && ssh sp-host 'date -u' && ssh idp-host 'date -u' |
| NameID format or value does not match what the SP expects | Common | xmllint --xpath "string(//*[local-name()='Subject']/*[local-name()='NameID']/@Format)" assertion.xml && xmllint --xpath "string(//*[local-name()='Subject']/*[local-name()='NameID'])" assertion.xml |
| SP is validating the wrong signature location (response vs assertion) or wrong issuer metadata | Less common | xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -v "count(//ds:Signature)" assertion.xml |
| Assertion was re-signed with a different cert chain or digest/signature algorithm unsupported by the SP | Less common | `xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//ds:SignatureMethod |
Step-by-step diagnosis
- Capture the actual SAML response and decode it.
- In browser dev tools, copy the
SAMLResponseform field from the POST to the ACS endpoint, or export a HAR. Then decode it:
- In browser dev tools, copy the
python3 - <<'PY'
import base64, sys, urllib.parse
raw = sys.stdin.read().strip()
raw = urllib.parse.unquote_plus(raw)
print(base64.b64decode(raw).decode('utf-8', 'replace'))
PY
- If the decoded XML is empty, truncated, or not XML, this is not one of the three causes; check proxy/body-size handling first.
- Save it as
assertion.xmland continue.
- Check whether signature verification fails with the cert the SP trusts.
- Export the IdP signing cert currently configured in the SP to
idp-signing.crt, then run:
- Export the IdP signing cert currently configured in the SP to
xmlsec1 --verify --pubkey-cert-pem idp-signing.crt assertion.xml
- If you see output shaped like:
func=xmlSecOpenSSLX509StoreVerify:file=x509vfy.c:line=389:obj=x509-store:subj=unknown:error=71:certificate verification failed:X509_verify_cert: subject=/CN=idp-signing
Error: signature failed
ERROR
SignedInfo References (ok/all): 0/1
Manifests References (ok/all): 0/0
Error: failed to verify file "assertion.xml"
this is your problem. Jump to `### Stale or wrong IdP signing certificate at the SP`.
- If verification succeeds, continue.
- Check timestamps in the assertion against both systems' UTC clocks.
- Extract time conditions:
xmlstarlet sel -t -m "//*[local-name()='Conditions']" -v @NotBefore -o " " -v @NotOnOrAfter -n assertion.xml
xmlstarlet sel -t -m "//*[local-name()='SubjectConfirmationData']" -v @NotBefore -o " " -v @NotOnOrAfter -n assertion.xml
- Compare with current UTC on both ends:
date -u && ssh sp-host 'date -u' && ssh idp-host 'date -u'
- If either host differs by more than ~120 seconds from the assertion window, jump to
### Clock skew breaks NotBefore or NotOnOrAfter validation. - If clocks are aligned, continue.
- Check NameID value and format.
- Extract them:
xmllint --xpath "string(//*[local-name()='Subject']/*[local-name()='NameID'])" assertion.xml; echo
xmllint --xpath "string(//*[local-name()='Subject']/*[local-name()='NameID']/@Format)" assertion.xml; echo
- If the value is empty, transient when the SP expects persistent, or email when the SP expects an immutable username/external ID, jump to
### NameID format or value does not match the SP expectation. - If it looks correct, continue.
- Check whether the SP expects a signature on the response, the assertion, or both.
- Count signatures and inspect issuer:
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -v "count(//ds:Signature)" -n assertion.xml
xmllint --xpath "string(//*[local-name()='Issuer'][1])" assertion.xml; echo
- If there is only one signature but your SP is configured to require the other location, or the issuer does not match the metadata entityID the SP trusts, jump to
### SP validates the wrong signature location or wrong issuer metadata.
- Check signature and digest algorithms for compatibility.
- Inspect algorithms:
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//ds:SignatureMethod|//ds:DigestMethod" -v "local-name()" -o ": " -v "@Algorithm" -n assertion.xml
- If you see SHA-1 where the SP now rejects SHA-1, or an algorithm your library version does not support, jump to
### Unsupported or unexpected signing algorithm/certificate chain.
Fixes
Stale or wrong IdP signing certificate at the SP
- Pull the current signing cert from IdP metadata and update the SP trust config. Generic metadata fetch:
curl -fsS 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 | awk 'BEGIN{print "-----BEGIN CERTIFICATE-----"}{print}{print "-----END CERTIFICATE-----"}' > idp-signing.crt
openssl x509 -in idp-signing.crt -noout -subject -issuer -dates -fingerprint -sha256
- Replace the cert in the SP's SAML config. If your SP uses a file-based config, it typically looks like:
{
"idp_entity_id": "https://idp.example.com/metadata",
"idp_sso_url": "https://idp.example.com/sso",
"idp_signing_cert_file": "/etc/app/saml/idp-signing.crt"
}
- Reload the app/service:
sudo systemctl restart app
- Trade-off: if your IdP publishes multiple signing certs during rollover, import all active signing certs if your SP supports it; pinning only one cert causes avoidable outages during rotation.
- Verify it worked:
xmlsec1 --verify --pubkey-cert-pem /etc/app/saml/idp-signing.crt assertion.xml
Clock skew breaks NotBefore or NotOnOrAfter validation
- Fix NTP on both hosts.
timedatectl status
sudo timedatectl set-ntp true
chronyc tracking || true
chronyc sources -v || true
- If
timedatectlshowsSystem clock synchronized: noor offset is large, force a sync using your NTP client tooling, then restart the app if it caches time validation state. - If your SP has an allowed skew setting, set a small tolerance such as 120 seconds, not 10+ minutes. Example config:
{
"saml": {
"accepted_clock_skew_seconds": 120
}
}
- Trade-off: increasing skew tolerance reduces false negatives during minor drift, but it also widens replay windows. Keep it tight and fix NTP instead.
- Verify it worked:
date -u && ssh sp-host 'date -u' && ssh idp-host 'date -u'
NameID format or value does not match the SP expectation
- Change the IdP claim/attribute mapping so
NameIDmatches the SP contract. Common values:- persistent opaque ID for stable account linking
- email address only if the SP explicitly keys users by email
- In your IdP's SAML app configuration, set NameID format to one of:
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
- If the SP is file-configured, align it there too. Example:
{
"saml": {
"nameid_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
"user_lookup_field": "external_id"
}
}
- If users were previously linked by email and you switch to persistent IDs, migrate mappings first or users will appear as new accounts.
⚠️ Changing NameID semantics can break account linking and auto-provisioning. Export current user-to-identifier mappings before rollout.
- Verify it worked:
xmllint --xpath "string(//*[local-name()='Subject']/*[local-name()='NameID'])" assertion.xml && echo
SP validates the wrong signature location or wrong issuer metadata
- Inspect whether the IdP signs the response, the assertion, or both, then align the SP validation mode.
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -v "count(/*[local-name()='Response']/ds:Signature)" -o " response-signatures\n" -v "count(//*[local-name()='Assertion']/ds:Signature)" -o " assertion-signatures\n" assertion.xml
- Update the SP config to trust the correct issuer/entityID and expected signature placement. Example:
{
"saml": {
"idp_entity_id": "https://idp.example.com/metadata",
"require_signed_assertion": true,
"require_signed_response": false
}
}
- Verify it worked:
xmllint --xpath "string(//*[local-name()='Issuer'][1])" assertion.xml; echo
Unsupported or unexpected signing algorithm/certificate chain
- Check your XML security library version and upgrade if it cannot validate the algorithm in use.
xmlsec1 --version
openssl version
- If the assertion uses SHA-1, switch the IdP signing algorithm to SHA-256 in the IdP SAML app config. If the SP is pinned to a cert chain that changed, update the trusted cert as in the certificate fix above.
- Example algorithm output to compare:
SignatureMethod: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
DigestMethod: http://www.w3.org/2001/04/xmlenc#sha256
- Verify it worked:
xmlstarlet sel -N ds="http://www.w3.org/2000/09/xmldsig#" -t -m "//ds:SignatureMethod|//ds:DigestMethod" -v "@Algorithm" -n assertion.xml
Prevention
- Monitor certificate expiry and fingerprint drift for IdP metadata in CI or a daily cron:
curl -fsS https://idp.example.com/metadata -o /tmp/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 /tmp/idp-metadata.xml | awk 'BEGIN{print "-----BEGIN CERTIFICATE-----"}{print}{print "-----END CERTIFICATE-----"}' | openssl x509 -noout -fingerprint -sha256 -dates
- Add a clock-drift alert on all SSO-relevant hosts. With chrony:
chronyc tracking | awk -F: '/Last offset/ {gsub(/ seconds/,"",$2); print $2}'
Alert if absolute offset exceeds 1 second; page at 30 seconds.
- Store the SP SAML contract in version control, including expected
entityID, ACS URL, NameID format, and allowed skew:
{
"entity_id": "https://sp.example.com/saml/metadata",
"acs_url": "https://sp.example.com/saml/acs",
"nameid_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
"accepted_clock_skew_seconds": 120
}
- Add a synthetic SSO validation job in staging that decodes a test assertion and checks signature, issuer, and NameID before deploy:
xmlsec1 --verify --pubkey-cert-pem ci/idp-signing.crt tests/fixtures/assertion.xml && xmllint --xpath "string(//*[local-name()='Subject']/*[local-name()='NameID']/@Format)" tests/fixtures/assertion.xml
- Log the exact SAML rejection reason at the SP boundary, but redact the assertion body in production logs. Keep issuer, NameID format, cert fingerprint, and time-window fields; drop PII-heavy attributes.
- During planned IdP cert rotation, preload the next signing cert in the SP if supported, then test in a non-production app before flipping production traffic.
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