decolua/9router · error
InResponseTo mismatch: expected ${expectedRequestId}, receiv
Error message
InResponseTo mismatch: expected ${expectedRequestId}, received ${inResponseTo || "none"} What it means
When an expectedRequestId is supplied (from the saml_state cookie), the function decodes the SAMLResponse XML and checks its InResponseTo attribute matches. SAML IdPs echo this request ID in responses to SP-initiated login; a mismatch means the response does not correspond to the outstanding auth request. This is a replay/CSRF protection: stale, forged, or unsolicited responses are rejected before full signature validation.
Source
Thrown at src/lib/auth/saml.js:155
const origin = getSamlBaseUrl(request, settings);
const samlInstance = createSamlInstance(settings, origin);
const container = typeof body === "object" && body !== null ? body : { SAMLResponse: body };
const rawSamlResponse = container.SAMLResponse;
if (!rawSamlResponse) {
throw new Error("Missing SAMLResponse parameter in assertion POST body");
}
// Parse response XML to inspect InResponseTo for replay protection
if (expectedRequestId) {
const xml = Buffer.from(rawSamlResponse, "base64").toString("utf8");
const match = xml.match(/InResponseTo=["']([^"']+)["']/i);
const inResponseTo = match ? match[1] : null;
if (!inResponseTo || inResponseTo !== expectedRequestId) {
throw new Error(`InResponseTo mismatch: expected ${expectedRequestId}, received ${inResponseTo || "none"}`);
}
}
const result = await samlInstance.validatePostResponseAsync({ SAMLResponse: rawSamlResponse });
const profile = result?.profile || result;
return profile;
}
/**
* Generates standard SP XML Metadata.
* @param {string} origin
* @param {object} settings
* @returns {string}
*/
export function generateSamlMetadata(origin, settings) {
const samlInstance = createSamlInstance(settings, origin);
return samlInstance.generateServiceProviderMetadata();View on GitHub (pinned to 90b52e06ff)
Solutions
- Restart the login flow: clear saml_state, hit /login again to mint a fresh request ID, and complete the IdP redirect in the same session/tab.
- Verify the saml_state cookie survives the IdP round trip (correct domain, SameSite=None+Secure for cross-site POST, not stripped by a proxy).
- For IdP-initiated SSO, do not pass expectedRequestId (or branch on flow type) since such responses legitimately omit InResponseTo.
- Avoid replaying cached SAMLResponse values in scripts/tests — each assertion is bound to one request ID; generate a new auth request per attempt.
Example fix
// before const result = await validateSamlResponse(req, body, staleStateId, settings); // stale cookie // after const stateId = req.cookies.saml_state; // read the cookie bound to THIS flow if (!stateId) return redirectToLogin(); // start a fresh SP-initiated request const result = await validateSamlResponse(req, body, stateId, settings);
Defensive patterns
Strategy: try-catch
Validate before calling
function inResponseToMatches(samlResponseBase64, expectedRequestId) {
if (!expectedRequestId) return true;
const xml = Buffer.from(samlResponseBase64, 'base64').toString('utf8');
const m = xml.match(/InResponseTo=["']([^"']+)["']/i);
return !!m && m[1] === expectedRequestId;
}
// pre-check before calling validateSamlResponse to give a friendly restart-login UX Type guard
function isSamlStateCookie(v) {
return typeof v === 'string' && /^[A-Za-z0-9_-]{8,}$/.test(v); // plausible request id
} Try / catch
try {
const profile = await validateSamlResponse(req, body, req.cookies.saml_state, settings);
} catch (err) {
if (String(err.message).startsWith('InResponseTo mismatch')) {
clearSamlStateCookie(res);
return res.status(400).json({ error: 'Login request expired or replayed — start a new login' });
}
throw err;
} Prevention
- Keep saml_state cookie settings compatible with a cross-site IdP POST (SameSite=None; Secure) so it survives the round trip.
- Treat this error as 'restart the login flow': always clear state and redirect to /login rather than retrying the same assertion.
- Never cache or replay SAMLResponse values in scripts, tests, or logs — each is bound to a single request ID.
- If you support IdP-initiated SSO, route it to a path that skips the expectedRequestId check instead of sharing the SP-initiated callback.
When it happens
Trigger: Replaying a previously captured SAMLResponse whose InResponseTo is an old request ID; posting an IdP-initiated (unsolicited) response that has no InResponseTo (null match); a saml_state cookie from a different/abandoned login attempt; the regex finds no InResponseTo because the attribute is absent or uses different casing/namespace.
Common situations: User opened two login tabs, completed one, and the other submitted with the first cookie; back-button resubmission of an old assertion; clock/flow issues causing IdP-initiated responses to hit an SP-initiated callback; multiple SAML requests queued so request IDs no longer line up.
Related errors
- IdP X.509 Certificate (samlCert) is missing or not configure
- Missing SAMLResponse parameter in assertion POST body
- Vertex partner models require a project_id. Add it in provid
- Vertex: failed to mint access token from Service Account JSO
- Vertex: failed to refresh access token from ADC JSON (author
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/937a9d743af7d3d9.
Report an issue: GitHub.