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

  1. Treat a second verification of the same challenge as success-or-prompt rather than error: track completed challenge ids client-side
  2. Disable resubmission after the first successful verify
  3. 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

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


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/dcf8624271bcb7e1. Report an issue: GitHub.