medusajs/medusa · error · MedusaError

MFA challenge has expired

Error message

MFA challenge has expired

What it means

Raised when verifying an MFA challenge whose expires_at timestamp is in the past. Challenges (from cache) have a limited TTL; an expired challenge must be re-created via a new authentication/MFA flow.

Source

Thrown at packages/modules/auth/src/services/auth-module.ts:834

    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"
      )
    }

    if (!challenge.methods.includes(method)) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `MFA challenge does not support method "${method}"`
      )
    }

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Catch this error and restart the MFA flow to issue a fresh challenge
  2. Show a 'code expired, resend' UI when the challenge TTL lapses
  3. Submit verification promptly after the challenge is created

Example fix

// before
await authModule.verifyAuthMfaChallenge({ challenge_id, method, body })
// after
try { await authModule.verifyAuthMfaChallenge({ challenge_id, method, body }) }
catch (e) { if (/expired/.test(e.message)) return restartMfaFlow() ; throw e }
Defensive patterns

Strategy: fallback

Validate before calling

const challenge = await authModule.retrieveAuthMfaChallenge(challengeId)
if (new Date(challenge.expires_at).getTime() <= Date.now()) return restartMfaFlow()

Type guard

null

Try / catch

try { await verify() } catch (e) { if (/expired/.test(e.message)) return restartMfaFlow(); throw e }

Prevention

When it happens

Trigger: User waits past the challenge TTL (e.g. 5 minutes) on the code-entry screen before submitting; clock differences; long-running tests that pause between create and verify.

Common situations: Idle users returning to a stale OTP form; backgrounded mobile app; test suites with artificial delays; retrying old challenges after cache restart.

Related errors


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