{"record":{"id":"0213c1f30ab52eb1","repo":"medusajs/medusa","slug":"an-active-totp-factor-already-exists-for-this-auth","errorCode":null,"errorMessage":"An active TOTP factor already exists for this auth identity","messagePattern":"An active TOTP factor already exists for this auth identity","errorType":"exception","errorClass":"MedusaError","httpStatus":403,"severity":"error","filePath":"packages/modules/auth/src/providers/mfa/totp.ts","lineNumber":84,"sourceCode":"\n  async start(\n    data: AuthTypes.AuthMfaStartDTO,\n    sharedContext: Context = {}\n  ): Promise<AuthTypes.AuthMfaStartResponse> {\n    const totpConfig = this.getTotpConfig_()\n    const issuer = data.issuer ?? totpConfig.issuer\n    const existingFactors = await this.authMfaFactorService_.list(\n      {\n        auth_identity_id: data.auth_identity_id,\n        provider: this.method,\n        status: [\"pending\", \"enabled\"],\n      },\n      { select: [\"id\"] },\n      sharedContext\n    )\n\n    if (existingFactors.length) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        \"An active TOTP factor already exists for this auth identity\"\n      )\n    }\n\n    const secret = generateTotpSecret()\n    const factor = await this.authMfaFactorService_.create(\n      {\n        auth_identity_id: data.auth_identity_id,\n        provider: this.method,\n        status: \"pending\",\n        provider_metadata: {\n          secret: encryptSecret(secret, this.getEncryptionKey_()),\n          issuer,\n        },\n        metadata: {\n          ...(data.metadata ?? {}),\n          label: data.label ?? undefined,","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/auth/src/providers/mfa/totp.ts#L66-L102","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Delete/disable the existing factor first (deleteAuthMfaFactor), then start again","If the existing factor is pending and orphaned, remove it via its id before re-initiating","Check listMfaFactors for the identity before showing the setup button"],"exampleFix":"// before\nawait authModule.startMfa('totp', { auth_identity_id: id })\n// after\nconst factors = await authModuleService.listAuthMfaFactors(id)\nfor (const f of factors.filter((f) => f.provider === 'totp')) {\n  await authModuleService.deleteAuthMfaFactor(f.id)\n}\nawait authModule.startMfa('totp', { auth_identity_id: id })","handlingStrategy":"validation","validationCode":"const factors = await authModuleService.listAuthMfaFactors(identityId)\nconst hasActiveTotp = factors.some((f) => f.provider === 'totp' && f.status !== 'disabled')\nif (!hasActiveTotp) await authModuleService.startMfa('totp', { auth_identity_id: identityId })","typeGuard":"const hasActiveTotp = (factors: { provider: string; status: string }[]) => factors.some((f) => f.provider === 'totp' && f.status !== 'disabled')","tryCatchPattern":"try { await start() } catch (e) { if (e.message.includes('already exists')) { /* offer reset flow: delete then restart */ } throw e }","preventionTips":["Check existing factors before showing setup UI","Clean up abandoned pending setups periodically"],"tags":["auth","mfa","totp"],"backgroundTag":"duplicate-resource","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}