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

  1. 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.
  2. Verify the saml_state cookie survives the IdP round trip (correct domain, SameSite=None+Secure for cross-site POST, not stripped by a proxy).
  3. For IdP-initiated SSO, do not pass expectedRequestId (or branch on flow type) since such responses legitimately omit InResponseTo.
  4. 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

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


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/937a9d743af7d3d9. Report an issue: GitHub.