medusajs/medusa · error · MedusaError

An active TOTP factor already exists for this auth identity

Error message

An active TOTP factor already exists for this auth identity

What it means

The TOTP MFA provider enforces a single active TOTP factor per auth identity. Calling start() when an enabled or pending TOTP factor already exists throws NOT_ALLOWED.

Source

Thrown at packages/modules/auth/src/providers/mfa/totp.ts:84

  async start(
    data: AuthTypes.AuthMfaStartDTO,
    sharedContext: Context = {}
  ): Promise<AuthTypes.AuthMfaStartResponse> {
    const totpConfig = this.getTotpConfig_()
    const issuer = data.issuer ?? totpConfig.issuer
    const existingFactors = await this.authMfaFactorService_.list(
      {
        auth_identity_id: data.auth_identity_id,
        provider: this.method,
        status: ["pending", "enabled"],
      },
      { select: ["id"] },
      sharedContext
    )

    if (existingFactors.length) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        "An active TOTP factor already exists for this auth identity"
      )
    }

    const secret = generateTotpSecret()
    const factor = await this.authMfaFactorService_.create(
      {
        auth_identity_id: data.auth_identity_id,
        provider: this.method,
        status: "pending",
        provider_metadata: {
          secret: encryptSecret(secret, this.getEncryptionKey_()),
          issuer,
        },
        metadata: {
          ...(data.metadata ?? {}),
          label: data.label ?? undefined,

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Delete/disable the existing factor first (deleteAuthMfaFactor), then start again
  2. If the existing factor is pending and orphaned, remove it via its id before re-initiating
  3. Check listMfaFactors for the identity before showing the setup button

Example fix

// before
await authModule.startMfa('totp', { auth_identity_id: id })
// after
const factors = await authModuleService.listAuthMfaFactors(id)
for (const f of factors.filter((f) => f.provider === 'totp')) {
  await authModuleService.deleteAuthMfaFactor(f.id)
}
await authModule.startMfa('totp', { auth_identity_id: id })
Defensive patterns

Strategy: validation

Validate before calling

const factors = await authModuleService.listAuthMfaFactors(identityId)
const hasActiveTotp = factors.some((f) => f.provider === 'totp' && f.status !== 'disabled')
if (!hasActiveTotp) await authModuleService.startMfa('totp', { auth_identity_id: identityId })

Type guard

const hasActiveTotp = (factors: { provider: string; status: string }[]) => factors.some((f) => f.provider === 'totp' && f.status !== 'disabled')

Try / catch

try { await start() } catch (e) { if (e.message.includes('already exists')) { /* offer reset flow: delete then restart */ } throw e }

Prevention

When it happens

Trigger: authModuleService.createAuthMfaFactor / provider start() for 'totp' on an identity that already has a TOTP factor in enabled or pending status — e.g. the user retries 'set up authenticator' without finishing or removing the previous setup.

Common situations: Abandoned pending setups left behind after the user never scanned/confirmed the QR code; UI allowing repeated clicks of 'add MFA' without checking existing factors.

Related errors


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