[PR #8583] fix(phone-number): deduplicate verification records on repeated OTP requests #16325

Closed
opened 2026-04-13 10:29:39 -05:00 by GiteaMirror · 0 comments
Owner

Original Pull Request: https://github.com/better-auth/better-auth/pull/8583

State: closed
Merged: No


Summary

  • Root cause: createVerificationValue inserts without dedup — when a user requests a second OTP before the first expires, a duplicate verification record is created. On databases with unique constraints (Postgres, MySQL), subsequent calls to updateVerificationByIdentifier or deleteVerificationByIdentifier fail with 500 errors because they assume one record per identifier.
  • Fix: Add a createOrReplaceVerification helper that deletes any existing verification for the identifier before creating a new one. Applied to all 3 createVerificationValue call sites in the phone-number plugin:
    • signInPhoneNumber (requireVerification flow)
    • sendPhoneNumberOTP
    • requestPasswordResetPhoneNumber
  • This matches the .catch() delete-and-retry pattern already used in the email-otp plugin (email-otp/routes.ts:117-134), but uses a slightly cleaner delete-first approach that works on all databases (including SQLite which doesn't enforce unique constraints).

Test plan

  • Added test: repeated sendOtp calls produce exactly 1 verification record (not 3)
  • Added test: wrong code → re-send OTP → correct latest code succeeds
  • Added test: repeated requestPasswordReset calls produce exactly 1 verification record (not 2)
  • All 29 tests pass (3 new + 26 existing, zero regressions)
  • No type errors introduced (tsc --noEmit clean for changed file)

Future improvements

  • Add unique: true to identifier in get-tables.ts (breaking change for existing DBs with duplicates — should be bundled with a minor version bump). Users who want to add it early can do so safely after this fix lands, and npx auth migrate won't overwrite it.
  • Extract createOrReplaceVerificationValue into InternalAdapter for all plugins
  • Fix the same latent bug in email-otp (6 of 7 calls missing dedup) and other plugins

Summary by cubic

Prevents duplicate verification records when a user requests multiple OTPs for the same phone number before the first expires. Ensures only the latest code is valid and avoids 500s across Postgres/MySQL/SQLite.

  • Bug Fixes
    • Added createOrReplaceVerification to delete any existing verification before creating a new one.
    • Applied to all phone-number flows: signInPhoneNumber, sendPhoneNumberOTP, and requestPasswordResetPhoneNumber.
    • Matches the cleanup pattern used in email-otp, using a delete-first approach that works on all databases.
    • Fixed downstream errors in updateVerificationByIdentifier/deleteVerificationByIdentifier that assumed a single record.
    • Added tests to ensure repeated OTP requests keep exactly one verification and that only the latest code works.

Written for commit 31c1361b47. Summary will update on new commits.

**Original Pull Request:** https://github.com/better-auth/better-auth/pull/8583 **State:** closed **Merged:** No --- ## Summary - **Root cause:** `createVerificationValue` inserts without dedup — when a user requests a second OTP before the first expires, a duplicate verification record is created. On databases with unique constraints (Postgres, MySQL), subsequent calls to `updateVerificationByIdentifier` or `deleteVerificationByIdentifier` fail with 500 errors because they assume one record per identifier. - **Fix:** Add a `createOrReplaceVerification` helper that deletes any existing verification for the identifier before creating a new one. Applied to all 3 `createVerificationValue` call sites in the phone-number plugin: - `signInPhoneNumber` (requireVerification flow) - `sendPhoneNumberOTP` - `requestPasswordResetPhoneNumber` - This matches the `.catch()` delete-and-retry pattern already used in the email-otp plugin (`email-otp/routes.ts:117-134`), but uses a slightly cleaner delete-first approach that works on all databases (including SQLite which doesn't enforce unique constraints). ## Test plan - [x] Added test: repeated `sendOtp` calls produce exactly 1 verification record (not 3) - [x] Added test: wrong code → re-send OTP → correct latest code succeeds - [x] Added test: repeated `requestPasswordReset` calls produce exactly 1 verification record (not 2) - [x] All 29 tests pass (3 new + 26 existing, zero regressions) - [x] No type errors introduced (`tsc --noEmit` clean for changed file) ## Future improvements - Add `unique: true` to `identifier` in `get-tables.ts` (breaking change for existing DBs with duplicates — should be bundled with a minor version bump). Users who want to add it early can do so safely after this fix lands, and `npx auth migrate` won't overwrite it. - Extract `createOrReplaceVerificationValue` into `InternalAdapter` for all plugins - Fix the same latent bug in email-otp (6 of 7 calls missing dedup) and other plugins <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Prevents duplicate verification records when a user requests multiple OTPs for the same phone number before the first expires. Ensures only the latest code is valid and avoids 500s across Postgres/MySQL/SQLite. - **Bug Fixes** - Added `createOrReplaceVerification` to delete any existing verification before creating a new one. - Applied to all `phone-number` flows: `signInPhoneNumber`, `sendPhoneNumberOTP`, and `requestPasswordResetPhoneNumber`. - Matches the cleanup pattern used in `email-otp`, using a delete-first approach that works on all databases. - Fixed downstream errors in `updateVerificationByIdentifier`/`deleteVerificationByIdentifier` that assumed a single record. - Added tests to ensure repeated OTP requests keep exactly one verification and that only the latest code works. <sup>Written for commit 31c1361b47c6bac1c48eedb6d6cb1b795ae36113. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
GiteaMirror added the pull-request label 2026-04-13 10:29:39 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#16325