[GH-ISSUE #8561] Email OTP: Case-sensitive email handling causes OTP verification failures #19751

Open
opened 2026-04-15 19:05:14 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @cursor[bot] on GitHub (Mar 12, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8561

Originally assigned to: @bytaesu on GitHub.

Description

The email-otp plugin has inconsistent email normalization across its endpoints. Some endpoints normalize ctx.body.email to lowercase before building the OTP identifier and querying the user, while others use the raw input as-is. Since OTP identifiers are case-sensitive strings, a casing mismatch between the request and verification steps causes valid OTPs to appear invalid.

Example Scenario

  1. User requests a password reset OTP with User@Example.com
  2. User attempts to reset their password with user@example.com
  3. Verification fails because identifiers do not match:
    • Stored: forget-password-otp-User@Example.com
    • Lookup: forget-password-otp-user@example.com

Affected Endpoints

Endpoints that properly normalize (✓):

  • /email-otp/send-verification-otp — uses ctx.body.email.toLowerCase()
  • /email-otp/verify-email — uses ctx.body.email.toLowerCase()
  • /sign-in/email-otp — uses rawEmail.toLowerCase()

Endpoints missing normalization (✗):

  • /email-otp/request-password-reset — uses raw ctx.body.email
  • /forget-password/email-otp — uses raw ctx.body.email
  • /email-otp/reset-password — uses raw ctx.body.email

Root Cause

The toOTPIdentifier() utility function (packages/better-auth/src/plugins/email-otp/utils.ts) passes the email through as-is without normalizing it. This means every call site must remember to normalize independently, which has been inconsistently applied.

Suggested Fix

Option A — Normalize at call sites (minimal change):

// In requestPasswordResetEmailOTP, forgetPasswordEmailOTP, resetPasswordEmailOTP
const email = ctx.body.email.toLowerCase();

Option B — Normalize inside toOTPIdentifier() (more robust):

export function toOTPIdentifier(
  type: "email-verification" | "sign-in" | "forget-password" | "change-email",
  email: string,
) {
  return `${type}-otp-${email.toLowerCase()}`;
}

Option B prevents the issue from recurring if new endpoints are added.

References

  • Flagged during PR #8560 review
Originally created by @cursor[bot] on GitHub (Mar 12, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8561 Originally assigned to: @bytaesu on GitHub. ## Description The email-otp plugin has **inconsistent email normalization** across its endpoints. Some endpoints normalize `ctx.body.email` to lowercase before building the OTP identifier and querying the user, while others use the raw input as-is. Since OTP identifiers are case-sensitive strings, a casing mismatch between the request and verification steps causes valid OTPs to appear invalid. ## Example Scenario 1. User requests a password reset OTP with `User@Example.com` 2. User attempts to reset their password with `user@example.com` 3. Verification fails because identifiers do not match: - Stored: `forget-password-otp-User@Example.com` - Lookup: `forget-password-otp-user@example.com` ## Affected Endpoints **Endpoints that properly normalize (✓):** - `/email-otp/send-verification-otp` — uses `ctx.body.email.toLowerCase()` - `/email-otp/verify-email` — uses `ctx.body.email.toLowerCase()` - `/sign-in/email-otp` — uses `rawEmail.toLowerCase()` **Endpoints missing normalization (✗):** - `/email-otp/request-password-reset` — uses raw `ctx.body.email` - `/forget-password/email-otp` — uses raw `ctx.body.email` - `/email-otp/reset-password` — uses raw `ctx.body.email` ## Root Cause The `toOTPIdentifier()` utility function (`packages/better-auth/src/plugins/email-otp/utils.ts`) passes the email through as-is without normalizing it. This means every call site must remember to normalize independently, which has been inconsistently applied. ## Suggested Fix **Option A — Normalize at call sites (minimal change):** ```typescript // In requestPasswordResetEmailOTP, forgetPasswordEmailOTP, resetPasswordEmailOTP const email = ctx.body.email.toLowerCase(); ``` **Option B — Normalize inside `toOTPIdentifier()` (more robust):** ```typescript export function toOTPIdentifier( type: "email-verification" | "sign-in" | "forget-password" | "change-email", email: string, ) { return `${type}-otp-${email.toLowerCase()}`; } ``` Option B prevents the issue from recurring if new endpoints are added. ## References - Flagged during PR #8560 review
GiteaMirror added the credentialsbug labels 2026-04-15 19:05:14 -05:00
Author
Owner

@bytaesu commented on GitHub (Mar 12, 2026):

I'll check this after merging PR #8560

<!-- gh-comment-id:4045333840 --> @bytaesu commented on GitHub (Mar 12, 2026): I'll check this after merging PR #8560
Author
Owner

@Prakash21singh commented on GitHub (Mar 13, 2026):

I'll check this after merging PR #8560

Hi, I noticed this issue and the suggested fix (normalizing email in toOTPIdentifier).

Since you mentioned you’re currently working on another PR, would you like me to work on this fix and open a PR for it? Happy to take it if it helps.

<!-- gh-comment-id:4056407763 --> @Prakash21singh commented on GitHub (Mar 13, 2026): > I'll check this after merging PR [#8560](https://github.com/better-auth/better-auth/pull/8560) Hi, I noticed this issue and the suggested fix (normalizing email in `toOTPIdentifier`). Since you mentioned you’re currently working on another PR, would you like me to work on this fix and open a PR for it? Happy to take it if it helps.
Author
Owner

@bytaesu commented on GitHub (Mar 17, 2026):

I'll check this after merging PR #8560

Hi, I noticed this issue and the suggested fix (normalizing email in toOTPIdentifier).

Since you mentioned you’re currently working on another PR, would you like me to work on this fix and open a PR for it? Happy to take it if it helps.

Hi @Prakash21singh,

Now that #8560 has been merged, it looks clean to work on this.
Would you like to take it on? Feel free to go ahead 😁
This probably won't be too complex.

<!-- gh-comment-id:4071775602 --> @bytaesu commented on GitHub (Mar 17, 2026): > > I'll check this after merging PR [#8560](https://github.com/better-auth/better-auth/pull/8560) > > Hi, I noticed this issue and the suggested fix (normalizing email in `toOTPIdentifier`). > > Since you mentioned you’re currently working on another PR, would you like me to work on this fix and open a PR for it? Happy to take it if it helps. Hi @Prakash21singh, Now that #8560 has been merged, it looks clean to work on this. Would you like to take it on? Feel free to go ahead 😁 This probably won't be too complex.
Author
Owner

@Prakash21singh commented on GitHub (Mar 17, 2026):

I'll check this after merging PR #8560

Hi, I noticed this issue and the suggested fix (normalizing email in toOTPIdentifier).
Since you mentioned you’re currently working on another PR, would you like me to work on this fix and open a PR for it? Happy to take it if it helps.

Hi @Prakash21singh,

Now that #8560 has been merged, it looks clean to work on this. Would you like to take it on? Feel free to go ahead 😁 This probably won't be too complex.

Thanks! I'll take a look at it and start working on the issue. If I run into anything unclear, I'll ask here.

<!-- gh-comment-id:4071815203 --> @Prakash21singh commented on GitHub (Mar 17, 2026): > > > I'll check this after merging PR [#8560](https://github.com/better-auth/better-auth/pull/8560) > > > > > > Hi, I noticed this issue and the suggested fix (normalizing email in `toOTPIdentifier`). > > Since you mentioned you’re currently working on another PR, would you like me to work on this fix and open a PR for it? Happy to take it if it helps. > > Hi [@Prakash21singh](https://github.com/Prakash21singh), > > Now that [#8560](https://github.com/better-auth/better-auth/pull/8560) has been merged, it looks clean to work on this. Would you like to take it on? Feel free to go ahead 😁 This probably won't be too complex. Thanks! I'll take a look at it and start working on the issue. If I run into anything unclear, I'll ask here.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19751