{"record":{"id":"9b43bd5c4560679d","repo":"phacility/phabricator","slug":"invalid-response-token-for-this-challenge-token-d","errorCode":null,"errorMessage":"Invalid response token for this challenge: token digest does not match stored digest.","messagePattern":"Invalid response token for this challenge: token digest does not match stored digest\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/auth/storage/PhabricatorAuthChallenge.php","lineNumber":213,"sourceCode":"        pht(\n          'This challenge already has a response token; you can not '.\n          'set a new response token.'));\n    }\n\n    if (preg_match('/ /', $token->openEnvelope())) {\n      throw new Exception(\n        pht(\n          'The response token for this challenge is invalid: response '.\n          'tokens may not include spaces.'));\n    }\n\n    $digest = PhabricatorHash::digestWithNamedKey(\n      $token->openEnvelope(),\n      self::TOKEN_DIGEST_KEY);\n\n    if ($this->responseDigest !== null) {\n      if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {\n        throw new Exception(\n          pht(\n            'Invalid response token for this challenge: token digest does '.\n            'not match stored digest.'));\n      }\n    } else {\n      $this->responseDigest = $digest;\n    }\n\n    $this->responseToken = $token;\n\n    return $this;\n  }\n\n  public function getResponseToken() {\n    return $this->responseToken;\n  }\n\n  public function setResponseDigest($value) {","sourceCodeStart":195,"sourceCodeEnd":231,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/auth/storage/PhabricatorAuthChallenge.php#L195-L231","documentation":"PhabricatorAuthChallenge::setResponseToken() digests the offered response token with PhabricatorHash::digestWithNamedKey(). If the challenge already carries a responseDigest (persisted on an earlier leg), the fresh digest must satisfy phutil_hashes_are_identical() against the stored one; any difference means the response token changed between requests and the exception fires.","triggerScenarios":"Calling setResponseToken() with a different token than the one already recorded for the challenge - replaying an old MFA form after the challenge was answered, double submission where the client minted a new token, or a client that regenerates tokens per attempt against the same stored challenge.","commonSituations":"Back-button resubmission of an MFA prompt; JS double-fire submitting two different token values; scripts replaying captured MFA responses; concurrent attempts on one challenge.","solutions":["Restart the authentication/MFA flow to obtain fresh challenges and matching response tokens","Make the client submit each challenge response exactly once, unmodified","Guard forms against double submission (disable submit on click) so a second, different token never reaches the server"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  $challenge->setResponseToken($token_envelope);\n} catch (Exception $ex) {\n  // Digest mismatch: the flow's state changed (replay/double submit).\n  // Abort this attempt and restart the MFA flow - do not resubmit the\n  // same challenge with another minted token.\n  return $this->newDialog()\n    ->setTitle(pht('MFA Session Expired'))\n    ->addSubmitButton(pht('Restart')); \n}","preventionTips":["Submit each MFA challenge response exactly once and never regenerate the token client-side","Disable submit buttons on first click to prevent double submissions with different tokens","On any MFA error, restart the whole flow to get fresh challenges rather than retrying pieces"],"tags":["mfa","phabricator","token-digest","security","replay"],"backgroundTag":"mfa-response-token-mismatch","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}