{"record":{"id":"dcf8624271bcb7e1","repo":"medusajs/medusa","slug":"mfa-challenge-has-already-been-completed","errorCode":null,"errorMessage":"MFA challenge has already been completed","messagePattern":"MFA challenge has already been completed","errorType":"exception","errorClass":"MedusaError","httpStatus":403,"severity":"error","filePath":"packages/modules/auth/src/services/auth-module.ts","lineNumber":827,"sourceCode":"    factor: InferEntityType<typeof AuthMfaFactor>\n  ): Promise<AuthTypes.AuthMfaDTO> {\n    const serialized = await this.baseRepository_.serialize<\n      AuthTypes.AuthMfaDTO & {\n        provider_metadata?: Record<string, unknown>\n      }\n    >(factor)\n\n    delete serialized.provider_metadata\n\n    return serialized\n  }\n\n  protected assertMfaChallengeCanBeVerified_(\n    challenge: AuthTypes.AuthMfaChallengeDTO,\n    method: AuthTypes.AuthMfaChallengeMethod\n  ): void {\n    if (challenge.completed_at) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        \"MFA challenge has already been completed\"\n      )\n    }\n\n    if (new Date(challenge.expires_at).getTime() <= Date.now()) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        \"MFA challenge has expired\"\n      )\n    }\n\n    if (challenge.attempts >= challenge.max_attempts) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        \"MFA challenge has too many failed attempts\"\n      )\n    }","sourceCodeStart":809,"sourceCodeEnd":845,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/auth/src/services/auth-module.ts#L809-L845","documentation":"Raised while verifying an MFA challenge when the challenge record already has completed_at set — it was successfully verified before and cannot be verified again (single-use challenge).","triggerScenarios":"Calling verifyAuthMfaChallenge twice with the same challenge id; replaying a successful verification request (double-click, retried HTTP request); client re-submitting after success due to missing state update.","commonSituations":"Frontend not disabling the submit button after success; HTTP retry middleware replaying POSTs; network timeout causing the client to resend an already-accepted verification.","solutions":["Treat a second verification of the same challenge as success-or-prompt rather than error: track completed challenge ids client-side","Disable resubmission after the first successful verify","Make verification endpoints idempotent-safe by catching NOT_ALLOWED and re-authenticating the user"],"exampleFix":"// before\nawait authModule.verifyAuthMfaChallenge({ challenge_id, method, body }) // may replay\n// after\nif (verifiedChallengeIds.has(challenge_id)) return // already done\nawait authModule.verifyAuthMfaChallenge({ challenge_id, method, body })\nverifiedChallengeIds.add(challenge_id)","handlingStrategy":"try-catch","validationCode":"const challenge = await authModule.retrieveAuthMfaChallenge(challengeId)\nif (challenge.completed_at) return alreadyVerified()","typeGuard":"null","tryCatchPattern":"try { await verify() } catch (e) { if (/already been completed/.test(e.message)) return ok(); throw e }","preventionTips":["Track completed challenge ids client-side","Disable submit after first success"],"tags":["mfa","challenge","single-use","auth-module"],"backgroundTag":"operation-already-completed","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}