{"record":{"id":"3fc14c8f5eb19fc3","repo":"RocketChat/Rocket.Chat","slug":"an-assertion-with-the-same-id-cannot-be-used-more","errorCode":null,"errorMessage":"An assertion with the same ID cannot be used more than once.","messagePattern":"An assertion with the same ID cannot be used more than once\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/server/lib/saml/lib/SAML.ts","lineNumber":528,"sourceCode":"\t\t\t\t\tthrow new Error('Unable to validate response url');\n\t\t\t\t}\n\n\t\t\t\tif (!profile) {\n\t\t\t\t\tthrow new Error('No user data collected from IdP response.');\n\t\t\t\t}\n\n\t\t\t\tconst baseExpireAt = profile.expireAt instanceof Date ? profile.expireAt : new Date(Date.now() + 300000);\n\n\t\t\t\tconst safeExpireAt = new Date(baseExpireAt.getTime() + (service.allowedClockDrift || 0));\n\n\t\t\t\tif (!profile.assertionId || !profile.issuer) {\n\t\t\t\t\tSAMLUtils.error({ msg: 'Invalid SAML response: missing Assertion ID or Issuer', profile });\n\t\t\t\t\tthrow new Error('Invalid SAML response: missing Assertion ID or Issuer.');\n\t\t\t\t}\n\n\t\t\t\tif (!(await SamlUsedAssertions.markUsed(profile.assertionId, profile.issuer, safeExpireAt))) {\n\t\t\t\t\tSAMLUtils.warn({ msg: 'SAML assertion replay detected', issuer: profile.issuer, assertionId: profile.assertionId });\n\t\t\t\t\tthrow new Error('An assertion with the same ID cannot be used more than once.');\n\t\t\t\t}\n\n\t\t\t\t// create a random token to store the login result\n\t\t\t\t// to test an IdP initiated login on localhost, use the following URL (assuming SimpleSAMLPHP on localhost:8080):\n\t\t\t\t// http://localhost:8080/simplesaml/saml2/idp/SSOService.php?spentityid=http://localhost:3000/_saml/metadata/test-sp\n\t\t\t\tconst credentialToken = Random.id();\n\n\t\t\t\tconst loginResult = {\n\t\t\t\t\tprofile,\n\t\t\t\t};\n\n\t\t\t\tawait this.storeCredential(credentialToken, loginResult);\n\n\t\t\t\tlet redirectPath = SAMLUtils.getValidationActionRedirectPath(credentialToken);\n\t\t\t\tif (loginClient) {\n\t\t\t\t\tredirectPath += `&loginClient=${loginClient}`;\n\t\t\t\t}\n","sourceCodeStart":510,"sourceCodeEnd":546,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/2a7de457074cbb4d4373fbd9a4e5bea292c9c764/apps/meteor/server/lib/saml/lib/SAML.ts#L510-L546","documentation":"SAML assertions must be consumed exactly once. Rocket.Chat records each assertion id + issuer in the SamlUsedAssertions collection with an expiry derived from the assertion lifetime plus allowed clock drift; when markUsed returns false the id was already recorded, so the response is treated as a replay: 'SAML assertion replay detected' is logged and this Error is thrown, aborting the login.","triggerScenarios":"The same SAMLResponse POST is submitted twice — refresh of the callback URL, duplicate form submit, back-button replay; an IdP or intermediary re-issuing an assertion with the same ID; clock drift large enough that a re-check lands inside the NotOnOrAfter + allowedClockDrift window.","commonSituations":"Users bookmarking or refreshing the ACS endpoint; browser autofill/double-click on the IdP login form; aggressive proxies or browsers re-POSTing; test harnesses reusing a captured assertion; misconfigured IdPs recycling assertion IDs.","solutions":["Start a fresh login by redirecting to the SAML login endpoint (/saml/login/<provider>) instead of replaying the callback POST","Fix clock drift: verify NTP on the Rocket.Chat server and the IdP so drift stays below SAML_Allowed_Clock_Drift","If the IdP itself reuses assertion IDs, correct it on the IdP side — IDs must be unique per issuer","In automated tests, request a new assertion per attempt rather than replaying a recorded one"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const result = await validateSamlLogin(samlResponse);\n  // proceed with credentialToken\n} catch (error) {\n  if (error instanceof Error && error.message.includes('assertion with the same ID')) {\n    // replayed response: do NOT retry with the same payload — restart the SAML flow\n    res.redirect(302, `/saml/login/${provider}`);\n    return;\n  }\n  throw error;\n}","preventionTips":["Never refresh or bookmark the SAML ACS/callback URL — always restart login from /saml/login/<provider>","Keep server and IdP clocks NTP-synced so drift stays below the allowed clock drift setting","In tests, fetch a fresh assertion for every attempt; never replay a recorded one","If an IdP recycles assertion IDs, fix it there — uniqueness is the IdP's contract"],"tags":["saml","sso","replay","assertion","security"],"backgroundTag":"saml-assertion-replay","analyzedSha":"2a7de457074cbb4d4373fbd9a4e5bea292c9c764","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}