[GH-ISSUE #6603] 2FA verification tokens not deleted after successful verification #19197

Closed
opened 2026-04-15 18:00:55 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @delfortrie on GitHub (Dec 8, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6603

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a user account
  2. Enable 2FA (TOTP) for the account
  3. Log out
  4. Log in with email/password - redirected to 2FA verification page
  5. Enter TOTP code and successfully complete 2FA
  6. Check verification table - the 2FA token record still exists

Current vs. Expected behavior

Current: After successful 2FA verification, the verification token record remains in the database. Tokens accumulate over time (one per login).

Expected: The verification token should be deleted from the database after successful 2FA verification

What version of Better Auth are you using?

1.4.1

System info

{
  "node": "22.x",
  "framework": "NestJS (backend), Next.js 15 (frontend)",
  "database": "PostgreSQL 18",
  "adapter": "Kysely"
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';

export const auth = betterAuth({
  database: dbPool, // PostgreSQL Pool

  verification: {
    modelName: 'verification_tokens',
    fields: {
      value: 'token',
      expiresAt: 'expires_at',
      createdAt: 'created_at',
      updatedAt: 'updated_at',
    },
  },

  plugins: [
    twoFactor({
      issuer: 'App Issuer',
      totpOptions: {
        digits: 6,
        period: 30,
      },
      schema: {
        twoFactor: {
          modelName: 'two_factor',
          fields: {
            userId: 'user_id',
            backupCodes: 'backup_codes',
            createdAt: 'created_at',
            updatedAt: 'updated_at',
          },
        },
        user: {
          fields: {
            twoFactorEnabled: 'two_factor_enabled',
          },
        },
      },
    }),
  ],
});

Additional context

After multiple successful 2FA logins by the same user, the verification table accumulates records:

             id              |        identifier        |              value               |         expires_at
-----------------------------+--------------------------+----------------------------------+----------------------------
 qaGwWRwanvGHtuEwvkQwL73h... | 2fa-QxskIsezStoEXFFr...  | enOzFdEx2NM72l52RwwXjqd7J2B1THqz | 2025-12-08 03:57:58.696+00
 ZrLgTVbNx6YaW6Kkds4Zotw...  | 2fa-ORcjeXVj_7MoCDVL...  | enOzFdEx2NM72l52RwwXjqd7J2B1THqz | 2025-12-08 03:58:16.851+00

The suggested fix is to delete the verification token record after successful 2FA verification in the verifyTOTP flow.

Originally created by @delfortrie on GitHub (Dec 8, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6603 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a user account 2. Enable 2FA (TOTP) for the account 3. Log out 4. Log in with email/password - redirected to 2FA verification page 5. Enter TOTP code and successfully complete 2FA 6. Check `verification` table - the 2FA token record still exists ### Current vs. Expected behavior **Current:** After successful 2FA verification, the verification token record remains in the database. Tokens accumulate over time (one per login). **Expected:** The verification token should be deleted from the database after successful 2FA verification ### What version of Better Auth are you using? 1.4.1 ### System info ```bash { "node": "22.x", "framework": "NestJS (backend), Next.js 15 (frontend)", "database": "PostgreSQL 18", "adapter": "Kysely" } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from 'better-auth'; import { twoFactor } from 'better-auth/plugins'; export const auth = betterAuth({ database: dbPool, // PostgreSQL Pool verification: { modelName: 'verification_tokens', fields: { value: 'token', expiresAt: 'expires_at', createdAt: 'created_at', updatedAt: 'updated_at', }, }, plugins: [ twoFactor({ issuer: 'App Issuer', totpOptions: { digits: 6, period: 30, }, schema: { twoFactor: { modelName: 'two_factor', fields: { userId: 'user_id', backupCodes: 'backup_codes', createdAt: 'created_at', updatedAt: 'updated_at', }, }, user: { fields: { twoFactorEnabled: 'two_factor_enabled', }, }, }, }), ], }); ``` ### Additional context After multiple successful 2FA logins by the same user, the verification table accumulates records: ``` id | identifier | value | expires_at -----------------------------+--------------------------+----------------------------------+---------------------------- qaGwWRwanvGHtuEwvkQwL73h... | 2fa-QxskIsezStoEXFFr... | enOzFdEx2NM72l52RwwXjqd7J2B1THqz | 2025-12-08 03:57:58.696+00 ZrLgTVbNx6YaW6Kkds4Zotw... | 2fa-ORcjeXVj_7MoCDVL... | enOzFdEx2NM72l52RwwXjqd7J2B1THqz | 2025-12-08 03:58:16.851+00 ``` The suggested fix is to delete the verification token record after successful 2FA verification in the `verifyTOTP` flow.
GiteaMirror added the lockedbug labels 2026-04-15 18:00:55 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Dec 8, 2025):

Thank you for the detailed issue report. It looks like after a successful 2FA (TOTP) verification, the expected behavior is for the verification token to be removed from the database; however, you’re observing that token records persist and accumulate. One next step is to confirm that the twoFactor plugin isn’t being overridden by any custom logic in your verifyTOTP flow. If you haven’t already, try testing with the default configuration to see if the token deletion occurs as expected. Additionally, reviewing your database adapter and migration setup is a good idea since different adapters or schema customizations might affect token cleanup. There has been similar feedback regarding token persistence in certain versions, so ensuring you’re running the latest minor release or patch might help resolve the issue [1].

Could you provide any related logs or confirm if any custom hooks might be interfering with the plugin’s cleanup process? Tag @better-auth-agent if you would like an answer from the Agent.

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3624875496 --> @better-auth-agent[bot] commented on GitHub (Dec 8, 2025): Thank you for the detailed issue report. It looks like after a successful 2FA (TOTP) verification, the expected behavior is for the verification token to be removed from the database; however, you’re observing that token records persist and accumulate. One next step is to confirm that the twoFactor plugin isn’t being overridden by any custom logic in your verifyTOTP flow. If you haven’t already, try testing with the default configuration to see if the token deletion occurs as expected. Additionally, reviewing your database adapter and migration setup is a good idea since different adapters or schema customizations might affect token cleanup. There has been similar feedback regarding token persistence in certain versions, so ensuring you’re running the latest minor release or patch might help resolve the issue [[1]](https://github.com/better-auth/better-auth/issues/6446). Could you provide any related logs or confirm if any custom hooks might be interfering with the plugin’s cleanup process? Tag @better-auth-agent if you would like an answer from the Agent. _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@dosubot[bot] commented on GitHub (Dec 8, 2025):

Hi @delfortrie! I'm Dosu and I’m helping the better-auth team.

This is a known issue: Better Auth v1.4.1 does not delete 2FA verification tokens from the database after successful TOTP verification, so tokens accumulate over time—this is not the intended or best-practice behavior and has been confirmed in multiple issues and by reviewing the code (example, example).

The verifyTOTP flow and its underlying verifyTwoFactor function do not call the internal adapter’s deleteVerificationValue or deleteVerificationByIdentifier methods after a successful verification, even though these methods exist (code reference). Other flows (like OTP and magic link) do delete tokens after use, which is considered best practice for security and database hygiene.

Workaround:
As an interim fix, you can patch your local Better Auth instance to call the appropriate deletion method after successful 2FA verification. For example, after a successful TOTP verification in the verifyTOTP or verifyTwoFactor handler, add:

await ctx.context.internalAdapter.deleteVerificationValue(verificationToken.id);
// or, to delete all tokens for the identifier:
await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken.identifier);

Insert this after the session is created and before returning the response. This will ensure tokens are cleaned up as expected and prevent accumulation.

Let me know if you want a more detailed code example or guidance on where to insert this in your flow.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3624908995 --> @dosubot[bot] commented on GitHub (Dec 8, 2025): <!-- Greeting --> Hi @delfortrie! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known issue: Better Auth v1.4.1 does not delete 2FA verification tokens from the database after successful TOTP verification, so tokens accumulate over time—this is not the intended or best-practice behavior and has been confirmed in multiple issues and by reviewing the code ([example](https://github.com/better-auth/better-auth/issues/1085), [example](https://github.com/better-auth/better-auth/issues/6192)). The verifyTOTP flow and its underlying verifyTwoFactor function do not call the internal adapter’s deleteVerificationValue or deleteVerificationByIdentifier methods after a successful verification, even though these methods exist ([code reference](https://github.com/better-auth/better-auth/blob/c4d272b20606e4e964a881b691ab7ab7d91730c8/packages/better-auth/src/db/internal-adapter.ts#L920-L1002)). Other flows (like OTP and magic link) do delete tokens after use, which is considered best practice for security and database hygiene. **Workaround:** As an interim fix, you can patch your local Better Auth instance to call the appropriate deletion method after successful 2FA verification. For example, after a successful TOTP verification in the verifyTOTP or verifyTwoFactor handler, add: ```ts await ctx.context.internalAdapter.deleteVerificationValue(verificationToken.id); // or, to delete all tokens for the identifier: await ctx.context.internalAdapter.deleteVerificationByIdentifier(verificationToken.identifier); ``` Insert this after the session is created and before returning the response. This will ensure tokens are cleaned up as expected and prevent accumulation. Let me know if you want a more detailed code example or guidance on where to insert this in your flow. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/6510554f-8d7e-4157-8e93-a7a21e06ef50?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20issue%3A%20Better%20Auth%20v1.4.1%20does%20not%20delete%202FA%20verification%20tokens%20from%20the%20database%20after%20successful%20TOTP%20verification%2C%20so%20tokens%20accumulate%20over%20time%E2%80%94this%20is%20not%20the%20intended%20or%20best-practice%20behavior%20and%20has%20been%20confirmed%20in%20multiple%20issues%20and%20by%20reviewing%20the%20code%20%28%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/1085%29%2C%20%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/6192%29%29.%0A%0AThe%20verifyTOTP%20flow%20and%20its%20underlying%20verifyTwoFactor%20function%20do%20not%20call%20the%20internal%20adapter%E2%80%99s%20deleteVerificationValue%20or%20deleteVerificationByIdentifier%20methods%20after%20a%20successful%20verification%2C%20even%20though%20these%20methods%20exist%20%28%5Bcode%20reference%5D%28https%3A//github.com/better-auth/better-auth/blob/c4d272b20606e4e964a881b691ab7ab7d91730c8/packages/better-auth/src/db/internal-adapter.ts%23L920-L1002%29%29.%20Other%20flows%20%28like%20OTP%20and%20magic%20link%29%20do%20delete%20tokens%20after%20use%2C%20which%20is%20considered%20best%20practice%20for%20security%20and%20database%20hygiene.%0A%0A%2A%2AWorkaround%3A%2A%2A%20%20%0AAs%20an%20interim%20fix%2C%20you%20can%20patch%20your%20local%20Better%20Auth%20instance%20to%20call%20the%20appropriate%20deletion%20method%20after%20successful%202FA%20verification.%20For%20example%2C%20after%20a%20successful%20TOTP%20verification%20in%20the%20verifyTOTP%20or%20verifyTwoFactor%20handler%2C%20add%3A%0A%0A%60%60%60ts%0Aawait%20ctx.context.internalAdapter.deleteVerificationValue%28verificationToken.id%29%3B%0A//%20or%2C%20to%20delete%20all%20tokens%20for%20the%20identifier%3A%0Aawait%20ctx.context.internalAdapter.deleteVerificationByIdentifier%28verificationToken.identifier%29%3B%0A%60%60%60%0A%0AInsert%20this%20after%20the%20session%20is%20created%20and%20before%20returning%20the%20response.%20This%20will%20ensure%20tokens%20are%20cleaned%20up%20as%20expected%20and%20prevent%20accumulation.%0A%0ALet%20me%20know%20if%20you%20want%20a%20more%20detailed%20code%20example%20or%20guidance%20on%20where%20to%20insert%20this%20in%20your%20flow.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6603)
Author
Owner

@delfortrie commented on GitHub (Dec 8, 2025):

I've submitted a fix for this: #6604

<!-- gh-comment-id:3625028818 --> @delfortrie commented on GitHub (Dec 8, 2025): I've submitted a fix for this: #6604
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19197