medusajs/medusa · error · MedusaError
MFA challenge has already been completed
Error message
MFA challenge has already been completed
What it means
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).
Source
Thrown at packages/modules/auth/src/services/auth-module.ts:827
factor: InferEntityType<typeof AuthMfaFactor>
): Promise<AuthTypes.AuthMfaDTO> {
const serialized = await this.baseRepository_.serialize<
AuthTypes.AuthMfaDTO & {
provider_metadata?: Record<string, unknown>
}
>(factor)
delete serialized.provider_metadata
return serialized
}
protected assertMfaChallengeCanBeVerified_(
challenge: AuthTypes.AuthMfaChallengeDTO,
method: AuthTypes.AuthMfaChallengeMethod
): void {
if (challenge.completed_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"MFA challenge has already been completed"
)
}
if (new Date(challenge.expires_at).getTime() <= Date.now()) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"MFA challenge has expired"
)
}
if (challenge.attempts >= challenge.max_attempts) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"MFA challenge has too many failed attempts"
)
}View on GitHub (pinned to 5e06e544a2)
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
Example fix
// before
await authModule.verifyAuthMfaChallenge({ challenge_id, method, body }) // may replay
// after
if (verifiedChallengeIds.has(challenge_id)) return // already done
await authModule.verifyAuthMfaChallenge({ challenge_id, method, body })
verifiedChallengeIds.add(challenge_id) Defensive patterns
Strategy: try-catch
Validate before calling
const challenge = await authModule.retrieveAuthMfaChallenge(challengeId) if (challenge.completed_at) return alreadyVerified()
Type guard
null
Try / catch
try { await verify() } catch (e) { if (/already been completed/.test(e.message)) return ok(); throw e } Prevention
- Track completed challenge ids client-side
- Disable submit after first success
When it happens
Trigger: 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.
Common situations: Frontend not disabling the submit button after success; HTTP retry middleware replaying POSTs; network timeout causing the client to resend an already-accepted verification.
Related errors
- MFA challenge has expired
- Auth identity does not have any enabled MFA methods
- MFA factor with id "${id}" was not found
- Recovery code count must be between 1 and 50
- Recovery code is invalid or already used
AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27).
Data as JSON: /api/errors/dcf8624271bcb7e1.
Report an issue: GitHub.