Regression: Email OTP does not throw USER_NOT_FOUND when email records do not exist #2791

Closed
opened 2026-03-13 10:20:29 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @D4ario0 on GitHub (Jan 30, 2026).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Set up EmailOTP Plugin
  2. Set DisableSignUp option to true
  3. Call sendVerificationOTP with type: "sign-in" with an email that is not registered (either on client or server)
  4. success! (that's the problem)

Current vs. Expected behavior

Expected behaviour: catch an APIError with message: ERROR_CODES.USER_NOT_FOUND

Actual Behaviour: status: 200

What version of Better Auth are you using?

1.4.17

System info

{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#1 SMP PREEMPT_DYNAMIC Tue, 21 Oct 2025 15:01:34 +0000",
    "release": "6.17.4-4-cachyos",
    "cpuCount": 8,
    "cpuModel": "11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz",
    "totalMemory": "15.36 GB",
    "freeMemory": "8.94 GB"
  },
  "node": {
    "version": "v25.2.1",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.3"
  },
  "frameworks": [
    {
      "name": "astro",
      "version": "5.17.1"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "1.4.17",
    "config": null,
    "error": "Cannot find module 'astro:db'\nRequire stack:\n- .../src/lib/auth.ts"
  }
}

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

Package

Auth config (if applicable)

import { betterAuth } from "better-auth/minimal";
import { emailOTP } from "better-auth/plugins";
import { db, Users, Sessions, Accounts, Verifications } from "astro:db";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { sendEmail } from "./emails";

export const AUTH = betterAuth({
    baseURL: import.meta.env.BETTER_AUTH_URL,
    database: drizzleAdapter(db, {
        provider: "sqlite",
        schema: {
            verifications: Verifications,
            users: Users,
            sessions: Sessions,
            accounts: Accounts,
        },
        usePlural: true,
    }),
    socialProviders: {
        google: {
            clientId: import.meta.env.GOOGLE_CLIENT_ID,
            clientSecret: import.meta.env.GOOGLE_CLIENT_SECRET,
        },
    },
    emailAndPassword: { requireEmailVerification: true, enabled: true },
    plugins: [
        emailOTP({
            disableSignUp: true,
            expiresIn: 60 * 15, // 15 minutes
            allowedAttempts: 3,
            async sendVerificationOTP({ email, otp, type }) {
                if (type != "sign-in")
                    throw new Error("Only sign-in OTPs are supported");

                sendEmail({
                    to: email,
                    subject: "Your email verification code",
                    text: `Your email verification code is: ${otp}`,
                });
            },
        }),
    ],
});

Additional context

This behaviour was discussed on:
PR: email-otp: throw USER_NOT_FOUND when sign-up is disabled #3223
Issue: Email OTP plugin logic issue #3202

Originally created by @D4ario0 on GitHub (Jan 30, 2026). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Set up EmailOTP Plugin 2. Set `DisableSignUp` option to `true` 3. Call `sendVerificationOTP` with `type: "sign-in"` with an email that is not registered (either on client or server) 4. success! (that's the problem) ### Current vs. Expected behavior Expected behaviour: catch an `APIError` with `message: ERROR_CODES.USER_NOT_FOUND` Actual Behaviour: status: 200 ### What version of Better Auth are you using? 1.4.17 ### System info ```bash { "system": { "platform": "linux", "arch": "x64", "version": "#1 SMP PREEMPT_DYNAMIC Tue, 21 Oct 2025 15:01:34 +0000", "release": "6.17.4-4-cachyos", "cpuCount": 8, "cpuModel": "11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz", "totalMemory": "15.36 GB", "freeMemory": "8.94 GB" }, "node": { "version": "v25.2.1", "env": "development" }, "packageManager": { "name": "bun", "version": "1.3.3" }, "frameworks": [ { "name": "astro", "version": "5.17.1" } ], "databases": null, "betterAuth": { "version": "1.4.17", "config": null, "error": "Cannot find module 'astro:db'\nRequire stack:\n- .../src/lib/auth.ts" } } ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth/minimal"; import { emailOTP } from "better-auth/plugins"; import { db, Users, Sessions, Accounts, Verifications } from "astro:db"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { sendEmail } from "./emails"; export const AUTH = betterAuth({ baseURL: import.meta.env.BETTER_AUTH_URL, database: drizzleAdapter(db, { provider: "sqlite", schema: { verifications: Verifications, users: Users, sessions: Sessions, accounts: Accounts, }, usePlural: true, }), socialProviders: { google: { clientId: import.meta.env.GOOGLE_CLIENT_ID, clientSecret: import.meta.env.GOOGLE_CLIENT_SECRET, }, }, emailAndPassword: { requireEmailVerification: true, enabled: true }, plugins: [ emailOTP({ disableSignUp: true, expiresIn: 60 * 15, // 15 minutes allowedAttempts: 3, async sendVerificationOTP({ email, otp, type }) { if (type != "sign-in") throw new Error("Only sign-in OTPs are supported"); sendEmail({ to: email, subject: "Your email verification code", text: `Your email verification code is: ${otp}`, }); }, }), ], }); ``` ### Additional context This behaviour was discussed on: PR: email-otp: throw USER_NOT_FOUND when sign-up is disabled #3223 Issue: Email OTP plugin logic issue #3202
GiteaMirror added the bug label 2026-03-13 10:20:29 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Jan 30, 2026):

It looks like you've encountered a regression issue with the Email OTP functionality in Better Auth. Specifically, when the DisableSignUp option is set to true, the system should return a USER_NOT_FOUND error if a non-registered email is used for the sendVerificationOTP call. However, it seems that a success status (200) is currently being returned instead.

This behavior was recently discussed in issues regarding the email OTP plugin, particularly in relation to the fix that aimed to prevent user enumeration when sign-up is disabled. The fix results in the system returning a success response for non-existent emails, without sending an OTP [1]. If you're seeking to have the expected behavior reinstated (returning an error for non-existent emails), it is advisable to follow up on the ongoing discussions or open a new issue for clarity.

Would you like guidance on how to open an issue or further details about using Email OTP with Better Auth?

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

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Jan 30, 2026): It looks like you've encountered a regression issue with the Email OTP functionality in Better Auth. Specifically, when the `DisableSignUp` option is set to `true`, the system should return a `USER_NOT_FOUND` error if a non-registered email is used for the `sendVerificationOTP` call. However, it seems that a success status (200) is currently being returned instead. This behavior was recently discussed in issues regarding the email OTP plugin, particularly in relation to the fix that aimed to prevent user enumeration when sign-up is disabled. The fix results in the system returning a success response for non-existent emails, without sending an OTP [[1]](https://www.better-auth.com/docs/plugins/email-otp). If you're seeking to have the expected behavior reinstated (returning an error for non-existent emails), it is advisable to follow up on the ongoing discussions or open a new issue for clarity. Would you like guidance on how to open an issue or further details about using Email OTP with Better Auth? _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/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dosubot[bot] commented on GitHub (Jan 30, 2026):

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

This is a confirmed regression in Better Auth v1.4.17. When using the Email OTP plugin with disableSignUp: true, calling sendVerificationOTP for an unregistered email returns a 200 success response instead of throwing a USER_NOT_FOUND error. This matches your observation and is due to the implementation in v1.4.17, where the code returns { success: true } instead of an error when the email isn't found and sign-up is disabled. This was originally done to prevent user enumeration attacks, but it makes it impossible for the client to distinguish between "user not found" and "OTP sent" scenarios (source, discussion).

A fix was merged in PR #3223 to restore the expected behavior: when disableSignUp is true and the email is not registered, the plugin should throw an APIError with USER_NOT_FOUND. This change is not present in v1.4.17, so you will need to update to a version that includes this PR to get the correct error handling.

If you cannot upgrade, a possible workaround is to use an after hook or custom middleware to intercept the request and handle missing users as needed, but be aware this reintroduces user enumeration risk (example).

References for further reading:

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

@dosubot[bot] commented on GitHub (Jan 30, 2026): <!-- Greeting --> Hi @D4ario0! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a confirmed regression in Better Auth v1.4.17. When using the Email OTP plugin with `disableSignUp: true`, calling `sendVerificationOTP` for an unregistered email returns a 200 success response instead of throwing a `USER_NOT_FOUND` error. This matches your observation and is due to the implementation in v1.4.17, where the code returns `{ success: true }` instead of an error when the email isn't found and sign-up is disabled. This was originally done to prevent user enumeration attacks, but it makes it impossible for the client to distinguish between "user not found" and "OTP sent" scenarios ([source](https://github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/better-auth/src/plugins/email-otp/routes.ts#L121-L746), [discussion](https://github.com/better-auth/better-auth/issues/3202)). A fix was merged in [PR #3223](https://github.com/better-auth/better-auth/pull/3223) to restore the expected behavior: when `disableSignUp` is true and the email is not registered, the plugin should throw an `APIError` with `USER_NOT_FOUND`. This change is not present in v1.4.17, so you will need to update to a version that includes this PR to get the correct error handling. If you cannot upgrade, a possible workaround is to use an after hook or custom middleware to intercept the request and handle missing users as needed, but be aware this reintroduces user enumeration risk ([example](https://github.com/better-auth/better-auth/issues/7348)). References for further reading: - [PR #3223 (fix implementation)](https://github.com/better-auth/better-auth/pull/3223) - [Issue #3202 (problem discussion)](https://github.com/better-auth/better-auth/issues/3202) - [Relevant code in v1.4.17](https://github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/better-auth/src/plugins/email-otp/routes.ts#L121-L746) <!-- 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/bb043229-ecba-4e41-a9c2-99b5438f233b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/bb043229-ecba-4e41-a9c2-99b5438f233b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/bb043229-ecba-4e41-a9c2-99b5438f233b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/bb043229-ecba-4e41-a9c2-99b5438f233b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/bb043229-ecba-4e41-a9c2-99b5438f233b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/bb043229-ecba-4e41-a9c2-99b5438f233b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/bb043229-ecba-4e41-a9c2-99b5438f233b?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%20confirmed%20regression%20in%20Better%20Auth%20v1.4.17.%20When%20using%20the%20Email%20OTP%20plugin%20with%20%60disableSignUp%3A%20true%60%2C%20calling%20%60sendVerificationOTP%60%20for%20an%20unregistered%20email%20returns%20a%20200%20success%20response%20instead%20of%20throwing%20a%20%60USER_NOT_FOUND%60%20error.%20This%20matches%20your%20observation%20and%20is%20due%20to%20the%20implementation%20in%20v1.4.17%2C%20where%20the%20code%20returns%20%60%7B%20success%3A%20true%20%7D%60%20instead%20of%20an%20error%20when%20the%20email%20isn%27t%20found%20and%20sign-up%20is%20disabled.%20This%20was%20originally%20done%20to%20prevent%20user%20enumeration%20attacks%2C%20but%20it%20makes%20it%20impossible%20for%20the%20client%20to%20distinguish%20between%20%22user%20not%20found%22%20and%20%22OTP%20sent%22%20scenarios%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/better-auth/src/plugins/email-otp/routes.ts%23L121-L746%29%2C%20%5Bdiscussion%5D%28https%3A//github.com/better-auth/better-auth/issues/3202%29%29.%0A%0AA%20fix%20was%20merged%20in%20%5BPR%20%233223%5D%28https%3A//github.com/better-auth/better-auth/pull/3223%29%20to%20restore%20the%20expected%20behavior%3A%20when%20%60disableSignUp%60%20is%20true%20and%20the%20email%20is%20not%20registered%2C%20the%20plugin%20should%20throw%20an%20%60APIError%60%20with%20%60USER_NOT_FOUND%60.%20This%20change%20is%20not%20present%20in%20v1.4.17%2C%20so%20you%20will%20need%20to%20update%20to%20a%20version%20that%20includes%20this%20PR%20to%20get%20the%20correct%20error%20handling.%0A%0AIf%20you%20cannot%20upgrade%2C%20a%20possible%20workaround%20is%20to%20use%20an%20after%20hook%20or%20custom%20middleware%20to%20intercept%20the%20request%20and%20handle%20missing%20users%20as%20needed%2C%20but%20be%20aware%20this%20reintroduces%20user%20enumeration%20risk%C2%A0%28%5Bexample%5D%28https%3A//github.com/better-auth/better-auth/issues/7348%29%29.%0A%0AReferences%20for%20further%20reading%3A%0A-%20%5BPR%20%233223%20%28fix%20implementation%29%5D%28https%3A//github.com/better-auth/better-auth/pull/3223%29%0A-%20%5BIssue%20%233202%20%28problem%20discussion%29%5D%28https%3A//github.com/better-auth/better-auth/issues/3202%29%0A-%20%5BRelevant%20code%20in%20v1.4.17%5D%28https%3A//github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/better-auth/src/plugins/email-otp/routes.ts%23L121-L746%29)&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/7696)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2791