{"record":{"id":"246da5d2e408bb4c","repo":"decolua/9router","slug":"missing-samlresponse-parameter-in-assertion-post-b","errorCode":null,"errorMessage":"Missing SAMLResponse parameter in assertion POST body","messagePattern":"Missing SAMLResponse parameter in assertion POST body","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/auth/saml.js","lineNumber":145,"sourceCode":" * @param {Request} request\n * @param {object} body - Parsed form body or object containing SAMLResponse\n * @param {string} expectedRequestId - Request ID stored in saml_state cookie\n * @param {object} settings\n * @returns {Promise<object>}\n */\nexport async function validateSamlResponse(request, body, expectedRequestId, settings) {\n  if (!settings?.samlCert) {\n    throw new Error(\"IdP X.509 Certificate (samlCert) is missing or not configured\");\n  }\n\n  const origin = getSamlBaseUrl(request, settings);\n  const samlInstance = createSamlInstance(settings, origin);\n\n  const container = typeof body === \"object\" && body !== null ? body : { SAMLResponse: body };\n  const rawSamlResponse = container.SAMLResponse;\n\n  if (!rawSamlResponse) {\n    throw new Error(\"Missing SAMLResponse parameter in assertion POST body\");\n  }\n\n  // Parse response XML to inspect InResponseTo for replay protection\n  if (expectedRequestId) {\n    const xml = Buffer.from(rawSamlResponse, \"base64\").toString(\"utf8\");\n    const match = xml.match(/InResponseTo=[\"']([^\"']+)[\"']/i);\n    const inResponseTo = match ? match[1] : null;\n\n    if (!inResponseTo || inResponseTo !== expectedRequestId) {\n      throw new Error(`InResponseTo mismatch: expected ${expectedRequestId}, received ${inResponseTo || \"none\"}`);\n    }\n  }\n\n  const result = await samlInstance.validatePostResponseAsync({ SAMLResponse: rawSamlResponse });\n  const profile = result?.profile || result;\n\n  return profile;\n}","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/auth/saml.js#L127-L163","documentation":"The SAML Web Browser SSO POST binding requires the IdP to send the base64-encoded assertion in a SAMLResponse form field. validateSamlResponse normalizes the body (wrapping non-object bodies as {SAMLResponse: body}) and throws when container.SAMLResponse is absent or empty, since there is nothing to decode, parse, or signature-verify.","triggerScenarios":"POSTing to the SAML callback with a body lacking a SAMLResponse field (empty form post, JSON without the key), body being an empty string, or the body parser consuming the stream before the handler reads it so SAMLResponse is undefined.","commonSituations":"The SPA sent a fetch/axios POST with JSON headers so the form-urlencoded IdP fields were not parsed; the callback URL was visited directly (GET/no body) during testing; a misconfigured IdP RelayState/ACS posted to the wrong endpoint; middleware (body limit, CSRF, raw-body capture) stripped or consumed the multipart/urlencoded body.","solutions":["Confirm the IdP Assertion Consumer Service (ACS) URL points at this endpoint so the assertion POST actually arrives here.","Ensure the route parses application/x-www-form-urlencoded bodies (e.g. req.formData() or the app's body parser) and passes the parsed object to validateSamlResponse.","Test by logging Object.keys(body) right before the call to verify SAMLResponse is present and non-empty.","If the SPA relays the assertion, forward req body as form-encoded (SAMLResponse=<...>&RelayState=<...>) rather than JSON."],"exampleFix":"// before\nawait validateSamlResponse(req, {}, stateId, settings); // no SAMLResponse\n// after\nconst form = await req.formData();\nconst body = Object.fromEntries(form.entries()); // { SAMLResponse: '...', RelayState: '...' }\nawait validateSamlResponse(req, body, stateId, settings);","handlingStrategy":"validation","validationCode":"function hasSamlResponse(body) {\n  const container = typeof body === 'object' && body !== null ? body : { SAMLResponse: body };\n  return typeof container.SAMLResponse === 'string' && container.SAMLResponse.trim() !== '';\n}\nif (!hasSamlResponse(body)) return res.status(400).json({ error: 'SAMLResponse missing from POST body' });","typeGuard":"function hasSamlResponseField(b) {\n  return !!b && typeof b === 'object' && typeof b.SAMLResponse === 'string' && b.SAMLResponse.length > 0;\n}","tryCatchPattern":"try {\n  const profile = await validateSamlResponse(req, body, stateId, settings);\n} catch (err) {\n  if (String(err.message).includes('Missing SAMLResponse')) {\n    return res.status(400).json({ error: 'Bad SAML callback: no SAMLResponse field in POST body' });\n  }\n  throw err;\n}","preventionTips":["Verify the IdP ACS URL matches this callback route exactly so the assertion POST lands here.","Ensure the route parses form-urlencoded bodies before reading SAMLResponse (the POST binding is form-encoded, not JSON).","Don't let middleware (body-size limits, raw-body capture, CSRF filters) consume or drop the POST body before the handler runs.","Log Object.keys(body) at the callback entry during integration testing to catch empty/misparsed bodies early."],"tags":["saml","sso","request-body","auth"],"backgroundTag":"missing-samlresponse-parameter","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}