Fix “InResponseTo does not match” SAML errors
In SP-initiated SAML login, your service provider (SP) sends the identity provider (IdP) an
AuthnRequest with a unique ID. The IdP copies that ID into the
InResponseTo attribute of its Response, and usually into the
SubjectConfirmationData of the assertion too. Before accepting the login, your SP checks
that it really sent a request with that ID. If it can’t find one, login fails with an InResponseTo
error.
What the error looks like
- node-saml and passport-saml:
InResponseTo is not valid, orInResponseTo is missing from responsewhen the Response has no InResponseTo at all. - python3-saml and ruby-saml:
The InResponseTo of the Response: …, does not match the ID of the AuthNRequest sent by the SP: … - Spring Security SAML 2.0: error code
invalid_in_response_to, withThe InResponseTo attribute […] does not match the ID of the authentication request […]orThe response contained an InResponseTo attribute […] but no saved authentication request was found
Common causes and fixes
The session cookie didn’t come back with the Response
Many SPs remember the request ID in the user’s session. The IdP returns the Response as a
cross-site form POST to your assertion consumer service (ACS), and browsers don’t send
cookies marked SameSite=Lax or SameSite=Strict with cross-site POSTs. Your
SP then sees a new, empty session and no saved request.
Fix: set SameSite=None; Secure on the session cookie that holds the request
ID, or store request IDs server-side instead of in the session.
The Response reached a different server
If request IDs are kept in memory and you run more than one instance of your app, the login can start on one instance and finish on another. node-saml, for example, keeps request IDs in an in-memory cache by default.
Fix: share request IDs between instances, for example in Redis through
node-saml’s cacheProvider option, or use sticky sessions.
The user started two logins
SPs that store the request ID in the session usually keep only the most recent one. When a user starts login in two tabs, or clicks the login button twice, the first Response no longer matches.
Fix: keep every outstanding request ID until it’s used or expires, rather than one per session.
The request ID expired or was already used
SPs forget request IDs after a while, and should forget them once they’re used. node-saml
expires them after requestIdExpirationPeriodMs (8 hours by default) and removes them when
a Response consumes them. Refreshing the ACS page or going back in the browser resubmits a Response
whose ID has already been used.
Fix: after a successful login, redirect away from the ACS so the POST isn’t resubmitted. Treat a reused ID as the error it is.
The login was IdP-initiated
When a user starts from the IdP’s portal, there was no AuthnRequest, so the Response has no
InResponseTo. SPs that require one reject it. In node-saml this is the
validateInResponseTo option: "always" rejects these Responses,
"ifPresent" accepts them, and "never" (the default) skips the check.
Fix: decide whether you support IdP-initiated login. If you do, see what to do instead below for accepting it safely.
Should you switch InResponseTo validation off?
Every library above lets you skip the check, and some skip it unless you ask for it. Turning it off makes the error go away, but your SP then accepts Responses it never asked for.
Why it’s tempting
- The error is intermittent and only happens in production, behind a load balancer or with real browsers’ cookie rules, so it’s hard to reproduce and easy to silence.
- Your customers want IdP-initiated login from their portal, and those Responses have no InResponseTo at all.
- Keeping request IDs means shared state: a session that survives the round trip, or a cache all your instances can reach.
How each library turns it off
- node-saml and passport-saml:
validateInResponseTo: "never", which is the default. The check only runs if you set"ifPresent"or"always". - python3-saml: the check only runs when you pass the request ID to
is_valid(request_data, request_id). Leave it out and nothing is compared. - ruby-saml: the check only runs when you pass
matches_request_idin the Response options. - Spring Security: there’s no flag. You build a
ResponseValidatorwithout itsInResponseToValidator, OpenSAML checks the assertion’s InResponseTo separately, which you configure throughsetAssertionValidator.
What goes wrong without it
- Replayed Responses are accepted. A Response stays valid until its
NotOnOrAftertime, often several minutes. Anyone who gets hold of one, from a log file, a proxy, a HAR file shared for debugging, or a shared computer’s browser history, can post it to your ACS and be logged in as that user. Unless your SP separately remembers the assertion IDs it has accepted, nothing stops it. - Login CSRF. An attacker logs in to the IdP as themselves, keeps their own valid Response, and makes a victim’s browser post it to your ACS from a page they control. The victim is now signed in to the attacker’s account, and anything they enter there, like files, payment details or messages, goes to the attacker. Checking InResponseTo against the request IDs stored in the victim’s own session stops this. A single cache shared by all users stops replays, but not this attack, because the attacker’s own request ID is in it.
- The real bug stays. The causes above, like cookies that don’t come back or IDs held by the wrong server, usually break other things too, and they’re now hidden.
What to do instead
- For intermittent errors, find the cause in the list above and fix that: send
the session cookie with
SameSite=None; Secure, share request IDs between instances, keep every outstanding ID, and redirect away from the ACS after login. - For IdP-initiated login, validate InResponseTo only when it’s present (
"ifPresent"in node-saml), and protect the unsolicited Responses you do accept: remember accepted assertion IDs until they expire, and keep the allowed clock skew small. Even better, don’t log the user in from an unsolicited Response. Start a normal SP-initiated login instead; the user already has a session at the IdP, so they come straight back with a Response your SP asked for. - For shared state, store request IDs where every instance can read them, such as your session store or Redis, and remove each ID once it’s used.
Reproduce it with SAMLkit
SAMLkit can act as a test IdP and send your SP exactly the Response you want, so you can see how your SP handles each case before your users do.
- Add SAMLkit to your application as an identity provider, using the metadata URL on the hosted IdP page.
- Start a login in your application. SAMLkit shows the AuthnRequest; choose Respond.
- Choose Edit XML and change the
InResponseTovalues on the Response and theSubjectConfirmationData, or remove them to simulate IdP-initiated login. SAMLkit signs the edited XML, so your SP gets past signature checks to the InResponseTo check. - Choose Send response.
- To test a request ID that’s already been used, open the response under Messages and choose Replay response.
Got a failing Response from production logs or your browser’s developer tools? Paste it into the SAML decoder to read its InResponseTo.