[GH-ISSUE #3742] Configurable Email-Address Change Verification Flow #18338

Open
opened 2026-04-15 16:46:13 -05:00 by GiteaMirror · 18 comments
Owner

Originally created by @steinitz on GitHub (Aug 1, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3742

Originally assigned to: @ping-maxwell on GitHub.

Is this suited for github?

  • Yes, this is suited for github

Currently, when users change their email address in Better Auth, the system automatically sends two verification emails:

  1. A verification email to the OLD email address (via sendVerificationEmail)
  2. A verification email to the NEW email address (via changeEmail.sendChangeEmailVerification)

This dual-verification approach creates significant real-world problems:

User Impact

  • Blocked email changes: Users who no longer have access to their old email address cannot complete the verification process and must contact support for manual email changes
  • Support burden: Organizations must handle support tickets for what should be a self-service user action
  • Confusing experience: Users receive two verification emails and may not understand why both are necessary
  • Security concerns: Users may ignore legitimate verification emails thinking they're duplicates

Developer Impact

  • Fragile workarounds required: Developers must implement URL-based detection (request?.url.includes('/profile')) to prevent unwanted verification emails
  • Undocumented coupling: The relationship between signup and email change verification flows is not clearly documented
  • Limited customization: No built-in way to control when old email verification is sent
  • Testing difficulties: Email verification flows are hard to test, making these issues harder to discover

Describe the solution you'd like

Primary Feature: sendOldEmailVerification Option

Add a new configuration option sendOldEmailVerification (default: true for backward compatibility) that allows developers to control whether verification emails are sent to the old email address during email changes:

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true
  },
  emailVerification: {
    sendOldEmailVerification: false, // NEW OPTION - prevents old email verification
    sendVerificationEmail: async ({ user, url }, request) => {
      // Would only be called for actual signups when sendOldEmailVerification: false
      console.log("Sending signup verification email");
      await sendCustomEmail(user.email, url);
    },
    sendChangeEmailVerification: async ({ user, newEmail, url }, request) => {
      // Always called for email changes - this is the primary customization point
      console.log("Sending email change verification");
      await sendCustomEmail(newEmail, url);
    }
  }
});

Current Workaround (Fragile)

Developers currently must implement URL-based detection to achieve similar behavior:

sendVerificationEmail: async ({ user, url }, request) => {
  // Workaround: Check URL to detect email change flow
  const isEmailChange = request?.url.includes('/profile')
  if (!isEmailChange) {
    // Only send verification email for actual signups, not email changes
    await sendCustomEmail(user.email, url);
  }
}

Secondary Improvements

  1. Context Detection: Provide a built-in way to detect email change vs signup contexts without URL inspection

  2. Clear Documentation: Document the email change flow, including when each email is sent and how to customize the behavior

  3. Better Debugging: Add context information to email handlers to make flow identification easier

  4. Migration Path: Provide clear guidance for existing applications that may rely on the current dual-email behavior

Describe alternatives you've considered

Alternative 1: Always Single Email

  • Pros: Simpler user experience
  • Cons: Breaking change for applications that rely on old email verification

Alternative 2: Configuration at Call Time

  • Pros: Maximum flexibility per email change
  • Cons: More complex API, harder to configure consistently

Alternative 3: Role-Based Configuration

  • Pros: Different behavior for different user types
  • Cons: Adds complexity without addressing the core use case

Additional context

Real-World Use Cases for Configurable Verification

  • Consumer applications: Users frequently lose access to old email addresses (work changes, expired accounts, etc.)
  • Internal tools: Old email verification adds unnecessary friction for authenticated users
  • Migration scenarios: Importing users from other systems where old emails may be invalid
  • Self-service portals: Authenticated users should be trusted to change their email without dual verification
  • Support reduction: Eliminate support tickets for users who can't access old email addresses

Current Behavior Analysis

  • Both sendVerificationEmail and sendChangeEmailVerification are called during email changes
  • The sendOnSignUp: false setting does NOT prevent email change verification (contrary to expectations)
  • sendChangeEmailVerification works correctly and is the primary customization point for new email verification
  • The coupling between signup and email change flows is undocumented and counterintuitive

Backward Compatibility Strategy

  • Default sendOldEmailVerification: true maintains current dual-email behavior
  • Existing applications continue working without changes
  • Migration is opt-in and controlled by developers
  • No breaking changes to existing APIs

Implementation Considerations

  • Should work seamlessly with existing changeEmail.sendChangeEmailVerification
  • Must eliminate the need for URL-based workarounds: request?.url.includes('/profile')
  • Should provide clear context to email handlers about the verification type
  • Consider adding a context parameter to email handlers: { context: 'signup' | 'email-change' }

Expected Benefits

  • Reduced support burden: Users can change emails without manual intervention
  • Better UX: Clear, single-purpose verification emails
  • Cleaner code: No need for fragile URL-based workarounds
  • Flexible security: Organizations can choose appropriate verification levels
  • Easier testing: More predictable email verification behavior

This feature would significantly improve both developer experience and user experience for email address changes while maintaining full backward compatibility for existing applications.

Originally created by @steinitz on GitHub (Aug 1, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3742 Originally assigned to: @ping-maxwell on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Currently, when users change their email address in Better Auth, the system automatically sends two verification emails: 1. A verification email to the OLD email address (via `sendVerificationEmail`) 2. A verification email to the NEW email address (via `changeEmail.sendChangeEmailVerification`) This dual-verification approach creates significant real-world problems: ### User Impact - **Blocked email changes**: Users who no longer have access to their old email address cannot complete the verification process and must contact support for manual email changes - **Support burden**: Organizations must handle support tickets for what should be a self-service user action - **Confusing experience**: Users receive two verification emails and may not understand why both are necessary - **Security concerns**: Users may ignore legitimate verification emails thinking they're duplicates ### Developer Impact - **Fragile workarounds required**: Developers must implement URL-based detection (`request?.url.includes('/profile')`) to prevent unwanted verification emails - **Undocumented coupling**: The relationship between signup and email change verification flows is not clearly documented - **Limited customization**: No built-in way to control when old email verification is sent - **Testing difficulties**: Email verification flows are hard to test, making these issues harder to discover ### Describe the solution you'd like ### Primary Feature: `sendOldEmailVerification` Option Add a new configuration option `sendOldEmailVerification` (default: `true` for backward compatibility) that allows developers to control whether verification emails are sent to the old email address during email changes: ```javascript export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: true }, emailVerification: { sendOldEmailVerification: false, // NEW OPTION - prevents old email verification sendVerificationEmail: async ({ user, url }, request) => { // Would only be called for actual signups when sendOldEmailVerification: false console.log("Sending signup verification email"); await sendCustomEmail(user.email, url); }, sendChangeEmailVerification: async ({ user, newEmail, url }, request) => { // Always called for email changes - this is the primary customization point console.log("Sending email change verification"); await sendCustomEmail(newEmail, url); } } }); ``` ### Current Workaround (Fragile) Developers currently must implement URL-based detection to achieve similar behavior: ```javascript sendVerificationEmail: async ({ user, url }, request) => { // Workaround: Check URL to detect email change flow const isEmailChange = request?.url.includes('/profile') if (!isEmailChange) { // Only send verification email for actual signups, not email changes await sendCustomEmail(user.email, url); } } ``` ### Secondary Improvements 1. **Context Detection**: Provide a built-in way to detect email change vs signup contexts without URL inspection 2. **Clear Documentation**: Document the email change flow, including when each email is sent and how to customize the behavior 3. **Better Debugging**: Add context information to email handlers to make flow identification easier 4. **Migration Path**: Provide clear guidance for existing applications that may rely on the current dual-email behavior ### Describe alternatives you've considered ### Alternative 1: Always Single Email - **Pros**: Simpler user experience - **Cons**: Breaking change for applications that rely on old email verification ### Alternative 2: Configuration at Call Time - **Pros**: Maximum flexibility per email change - **Cons**: More complex API, harder to configure consistently ### Alternative 3: Role-Based Configuration - **Pros**: Different behavior for different user types - **Cons**: Adds complexity without addressing the core use case ### Additional context ### Real-World Use Cases for Configurable Verification - **Consumer applications**: Users frequently lose access to old email addresses (work changes, expired accounts, etc.) - **Internal tools**: Old email verification adds unnecessary friction for authenticated users - **Migration scenarios**: Importing users from other systems where old emails may be invalid - **Self-service portals**: Authenticated users should be trusted to change their email without dual verification - **Support reduction**: Eliminate support tickets for users who can't access old email addresses ### Current Behavior Analysis - Both `sendVerificationEmail` and `sendChangeEmailVerification` are called during email changes - The `sendOnSignUp: false` setting does NOT prevent email change verification (contrary to expectations) - `sendChangeEmailVerification` works correctly and is the primary customization point for new email verification - The coupling between signup and email change flows is undocumented and counterintuitive ### Backward Compatibility Strategy - Default `sendOldEmailVerification: true` maintains current dual-email behavior - Existing applications continue working without changes - Migration is opt-in and controlled by developers - No breaking changes to existing APIs ### Implementation Considerations - Should work seamlessly with existing `changeEmail.sendChangeEmailVerification` - Must eliminate the need for URL-based workarounds: `request?.url.includes('/profile')` - Should provide clear context to email handlers about the verification type - Consider adding a `context` parameter to email handlers: `{ context: 'signup' | 'email-change' }` ### Expected Benefits - **Reduced support burden**: Users can change emails without manual intervention - **Better UX**: Clear, single-purpose verification emails - **Cleaner code**: No need for fragile URL-based workarounds - **Flexible security**: Organizations can choose appropriate verification levels - **Easier testing**: More predictable email verification behavior This feature would significantly improve both developer experience and user experience for email address changes while maintaining full backward compatibility for existing applications.
GiteaMirror added the awaiting external contributorhelp wantedcredentials labels 2026-04-15 16:46:13 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 1, 2025):

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

Your feature request is not currently supported in Better Auth. There is no built-in sendOldEmailVerification option or similar configuration to control whether verification emails are sent to the old email address during email changes. The email verification and change email handlers (sendVerificationEmail and sendChangeEmailVerification) do not receive an explicit context parameter to distinguish between signup and email change flows, so developers often rely on workarounds like inspecting the request URL or token payload to infer context, which is fragile and undocumented [code reference].

Recent code and documentation clarify how these handlers are invoked, but there is no migration guide or official documentation for customizing or suppressing dual-verification emails during email changes. The current flow is determined by which handler is called, and there is no granular configuration for suppressing old email verification [code reference].

Until a feature like sendOldEmailVerification is implemented, the only workaround is to add custom logic in your sendVerificationEmail handler to detect and suppress unwanted emails, typically by inspecting the request or token. This is a known pain point and your proposal for explicit configuration and context would address a real gap in the current API.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3145123396 --> @dosubot[bot] commented on GitHub (Aug 1, 2025): <!-- Greeting --> Hi @steinitz! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your feature request is not currently supported in Better Auth. There is no built-in sendOldEmailVerification option or similar configuration to control whether verification emails are sent to the old email address during email changes. The email verification and change email handlers (sendVerificationEmail and sendChangeEmailVerification) do not receive an explicit context parameter to distinguish between signup and email change flows, so developers often rely on workarounds like inspecting the request URL or token payload to infer context, which is fragile and undocumented [[code reference](https://github.com/better-auth/better-auth/blob/7fa90f8d4da552ce343f2a5edd84de03d53540d1/packages/better-auth/src/types/options.ts)]. Recent code and documentation clarify how these handlers are invoked, but there is no migration guide or official documentation for customizing or suppressing dual-verification emails during email changes. The current flow is determined by which handler is called, and there is no granular configuration for suppressing old email verification [[code reference](https://github.com/better-auth/better-auth/blob/7fa90f8d4da552ce343f2a5edd84de03d53540d1/packages/better-auth/src/api/routes/update-user.ts)]. Until a feature like sendOldEmailVerification is implemented, the only workaround is to add custom logic in your sendVerificationEmail handler to detect and suppress unwanted emails, typically by inspecting the request or token. This is a known pain point and your proposal for explicit configuration and context would address a real gap in the current API. <!-- 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/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/8b9127be-eb9b-4600-8dcd-1c0a333b9e9f?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3742)
Author
Owner

@himself65 commented on GitHub (Aug 1, 2025):

for a real world practice, the reason of sendVerificationEmail is the account get hacked and avoid you lost the account by just switching the email address from hacker. So IMO, verify from old email is important in general.

I think we shouldn't provide an explicit field to skip this. But you defenitely can call verifyEmail inside sendVerificationEmail to flip the emailVerified value on database

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  emailVerification: {
    async sendVerificationEmail({ user, url, token }) {
      // auto verification, this could be dangerous
      await auth.api.verifyEmail({
        query: {
          token,
        },
      });
    },
  },
});

nit: I think we could let user customize own verify method, not just only email, maybe phone number in the future?

<!-- gh-comment-id:3145669366 --> @himself65 commented on GitHub (Aug 1, 2025): for a real world practice, the reason of `sendVerificationEmail` is the account get hacked and avoid you lost the account by just switching the email address from hacker. So IMO, verify from old email is important in general. I think we shouldn't provide an explicit field to skip this. But you defenitely can call `verifyEmail` inside `sendVerificationEmail` to flip the `emailVerified` value on database ```ts export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: true, }, emailVerification: { async sendVerificationEmail({ user, url, token }) { // auto verification, this could be dangerous await auth.api.verifyEmail({ query: { token, }, }); }, }, }); ``` nit: I think we could let user customize own verify method, not just only email, maybe phone number in the future?
Author
Owner

@frectonz commented on GitHub (Sep 2, 2025):

@steinitz I looked into the changeEmail implementation but it seems like there is no case in which two emails get sent to change an email. You can look at the implementation here.

There are basically two cases

  • If the user’s current email is unverified, the email is updated immediately and (optionally) a verification email is sent to the new address.
  • If the user’s current email is verified, the system does not update immediately but sends a change-verification email instead.
<!-- gh-comment-id:3246671250 --> @frectonz commented on GitHub (Sep 2, 2025): @steinitz I looked into the `changeEmail` implementation but it seems like there is no case in which two emails get sent to change an email. You can look at the implementation [here](https://github.com/better-auth/better-auth/blob/74e94c72f01d3345eed36b5b4a1e2071e3c8761d/packages/better-auth/src/api/routes/update-user.ts#L649). There are basically two cases - If the user’s current email is unverified, the email is updated immediately and (optionally) a verification email is sent to the new address. - If the user’s current email is verified, the system does not update immediately but sends a change-verification email instead.
Author
Owner

@steinitz commented on GitHub (Sep 2, 2025):

@frectonz Thanks for looking into this, I think...

I can confirm that I've seen two emails during my early development of the app. Unless the code has changed recently you will easily reproduce this. As indicated above and elsewhere it's a well-known feature, added intentionally.

I've managed to hack a workaround but it's fragile.

Honestly, I'd prefer not to think about this further. I've spent enough time working around it, thinking about it, burning AI tokens, asking questions, complicating my E2E testing and raising this detailed issue.

<!-- gh-comment-id:3246990192 --> @steinitz commented on GitHub (Sep 2, 2025): @frectonz Thanks for looking into this, I think... I can confirm that I've seen two emails during my early development of the app. Unless the code has changed recently you will easily reproduce this. As indicated above and elsewhere it's a well-known feature, added intentionally. I've managed to hack a workaround but it's fragile. Honestly, I'd prefer not to think about this further. I've spent enough time working around it, thinking about it, burning AI tokens, asking questions, complicating my E2E testing and raising this detailed issue.
Author
Owner

@Cali93 commented on GitHub (Sep 18, 2025):

I can confirm that when an existing user with a verified email changes their email, they will receive 2 emails:

  • One triggered by sendChangeEmailVerification
  • One triggered by sendVerificationEmail after the user successfully changed their email
<!-- gh-comment-id:3307338188 --> @Cali93 commented on GitHub (Sep 18, 2025): I can confirm that when an existing user with a verified email changes their email, they will receive 2 emails: - One triggered by `sendChangeEmailVerification` - One triggered by `sendVerificationEmail` after the user successfully changed their email
Author
Owner

@ixartz commented on GitHub (Oct 13, 2025):

@himself65

"is the account get hacked and avoid you lost the account by just switching the email address from hacker. So IMO, verify from old email is important in general."

It would be great to verify with password instead of verify from old email. Like for changing password, the users need to enter his old password. So to change his email, he needs to verify the password.

If the password is not enough, we can add MFA.

One problem with verifying with the old email is that the user can lose access to his old email, so he won't be able to change it.

<!-- gh-comment-id:3398966820 --> @ixartz commented on GitHub (Oct 13, 2025): @himself65 "is the account get hacked and avoid you lost the account by just switching the email address from hacker. So IMO, verify from old email is important in general." It would be great to verify with password instead of verify from old email. Like for changing password, the users need to enter his old password. So to change his email, he needs to verify the password. If the password is not enough, we can add MFA. One problem with verifying with the old email is that the user can lose access to his old email, so he won't be able to change it.
Author
Owner

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

Hi, @steinitz. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You requested a configurable option sendOldEmailVerification to disable sending verification emails to the old email during email changes.
  • Currently, no built-in option exists; workarounds rely on fragile custom logic in sendVerificationEmail handlers.
  • Discussion highlighted the security rationale for verifying the old email to prevent hijacking, with alternatives like password verification or MFA suggested.
  • You acknowledged the pain point and have implemented a fragile workaround but prefer an official configurable solution.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open.
  • Otherwise, this issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

<!-- gh-comment-id:3739360395 --> @dosubot[bot] commented on GitHub (Jan 12, 2026): Hi, @steinitz. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You requested a configurable option `sendOldEmailVerification` to disable sending verification emails to the old email during email changes. - Currently, no built-in option exists; workarounds rely on fragile custom logic in `sendVerificationEmail` handlers. - Discussion highlighted the security rationale for verifying the old email to prevent hijacking, with alternatives like password verification or MFA suggested. - You acknowledged the pain point and have implemented a fragile workaround but prefer an official configurable solution. **Next Steps:** - Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open. - Otherwise, this issue will be automatically closed in 7 days. Thank you for your understanding and contribution!
Author
Owner

@steinitz commented on GitHub (Jan 14, 2026):

Issue is still relevant and should be addressed with reasonable priorityOn 13 Jan 2026, at 3:14 am, dosubot[bot] @.***> wrote:dosubot[bot] left a comment (better-auth/better-auth#3742)
Hi, @steinitz. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.
Issue Summary:

You requested a configurable option sendOldEmailVerification to disable sending verification emails to the old email during email changes.
Currently, no built-in option exists; workarounds rely on fragile custom logic in sendVerificationEmail handlers.
Discussion highlighted the security rationale for verifying the old email to prevent hijacking, with alternatives like password verification or MFA suggested.
You acknowledged the pain point and have implemented a fragile workaround but prefer an official configurable solution.

Next Steps:

Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open.
Otherwise, this issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>

<!-- gh-comment-id:3747280495 --> @steinitz commented on GitHub (Jan 14, 2026): Issue is still relevant and should be addressed with reasonable priorityOn 13 Jan 2026, at 3:14 am, dosubot[bot] ***@***.***> wrote:dosubot[bot] left a comment (better-auth/better-auth#3742) Hi, @steinitz. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale. Issue Summary: You requested a configurable option sendOldEmailVerification to disable sending verification emails to the old email during email changes. Currently, no built-in option exists; workarounds rely on fragile custom logic in sendVerificationEmail handlers. Discussion highlighted the security rationale for verifying the old email to prevent hijacking, with alternatives like password verification or MFA suggested. You acknowledged the pain point and have implemented a fragile workaround but prefer an official configurable solution. Next Steps: Please let me know if this issue is still relevant to the latest version of better-auth by commenting here to keep the discussion open. Otherwise, this issue will be automatically closed in 7 days. Thank you for your understanding and contribution! —Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: ***@***.***>
Author
Owner

@qodesmith commented on GitHub (Feb 27, 2026):

@steinitz I can confirm this is still an issue. I'm using the latest version of Better Auth - 1.4.19.

For more context, I was debugging with Claude and this is what it gave me. Maybe pass this on to the Better Auth team:


Better Auth treats sendChangeEmailVerification as a fallback for sendChangeEmailConfirmation, triggering a 2-step process:

Step 1 (what you're seeing): Since user.emailVerified === true and sendChangeEmailVerification is configured, Better Auth creates a token with requestType: "change-email-confirmation" (line 461 of update-user.mjs) and sends the email via your sendChangeEmailVerification function.

When you click that link: The /verify-email endpoint sees requestType: "change-email-confirmation" (line 180 of email-verification.mjs) and instead of updating the email, it:

  1. Creates a second token with requestType: "change-email-verification"
  2. Sends a second email via emailVerification.sendVerificationEmail (your SignUpVerificationEmail template)
  3. Redirects you to /account

Step 2 (what you're missing): Only when the second email link is clicked does Better Auth actually update the email in the DB (line 206).

So you're clicking link 1 of 2. The second email is being sent using your SignUpVerificationEmail template (subject: "Verify your email address"),which probably looks unrelated to the email change.

Fix options:

  1. Remove sendChangeEmailVerification from user.changeEmail — this skips the 2-step flow and goes directly to a single-step verification via
    emailVerification.sendVerificationEmail. Downside: uses the sign-up email template.
  2. Keep the 2-step flow but check for that second email arriving after you click the first link.
<!-- gh-comment-id:3970358918 --> @qodesmith commented on GitHub (Feb 27, 2026): @steinitz I can confirm this is still an issue. I'm using the latest version of Better Auth - `1.4.19`. For more context, I was debugging with Claude and this is what it gave me. Maybe pass this on to the Better Auth team: --- Better Auth treats `sendChangeEmailVerification` as a fallback for `sendChangeEmailConfirmation`, triggering a 2-step process: **Step 1** (what you're seeing): Since `user.emailVerified === true` and `sendChangeEmailVerification` is configured, Better Auth creates a token with `requestType: "change-email-confirmation"` (line 461 of update-user.mjs) and sends the email via your `sendChangeEmailVerification` function. **When you click that link:** The `/verify-email` endpoint sees `requestType: "change-email-confirmation"` (line 180 of email-verification.mjs) and instead of updating the email, it: 1. Creates a second token with `requestType: "change-email-verification"` 2. Sends a second email via `emailVerification.sendVerificationEmail` (your `SignUpVerificationEmail` template) 3. Redirects you to `/account` **Step 2** (what you're missing): Only when the second email link is clicked does Better Auth actually update the email in the DB (line 206). So you're clicking link 1 of 2. The second email is being sent using your `SignUpVerificationEmail` template (subject: "Verify your email address"),which probably looks unrelated to the email change. Fix options: 1. Remove `sendChangeEmailVerification` from `user.changeEmail` — this skips the 2-step flow and goes directly to a single-step verification via `emailVerification.sendVerificationEmail`. Downside: uses the sign-up email template. 2. Keep the 2-step flow but check for that second email arriving after you click the first link.
Author
Owner

@steinitz commented on GitHub (Feb 27, 2026):

@qodesmith
From your report, Claude seemed to overlook that the user may no longer have access to the old email. I consider that the core defect. The inconvenience, spam, gratuitousness and possible confusion seem annoying but not fatal.

<!-- gh-comment-id:3973259544 --> @steinitz commented on GitHub (Feb 27, 2026): @qodesmith From your report, Claude seemed to overlook that the user may no longer have access to the old email. I consider that the core defect. The inconvenience, spam, gratuitousness and possible confusion seem annoying but not fatal.
Author
Owner

@qodesmith commented on GitHub (Feb 27, 2026):

@steinitz Interesting, I hadn't considered that. So it seems there are 2 scenarios here:

  1. User has access to old and new emails and would like to change their email on file
  2. User no longer has access to old email and would like to change their email on file

Is Better Auth trying to address both or just one of these scenarios?

<!-- gh-comment-id:3975373905 --> @qodesmith commented on GitHub (Feb 27, 2026): @steinitz Interesting, I hadn't considered that. So it seems there are 2 scenarios here: 1. User has access to old and new emails and would like to change their email on file 2. User no longer has access to old email and would like to change their email on file Is Better Auth trying to address both or just one of these scenarios?
Author
Owner

@qodesmith commented on GitHub (Feb 27, 2026):

For the record, I was able to mitigate against this issue by:

  1. Not using user.changeEmail.sendChangeEmailVerification
  2. Implementing conditional login inside emailVerification.sendVerificationEmail

Example sendVerificationEmail implementation:

const authOptions = {
  emailVerification: {
    sendVerificationEmail: async ({user, url, token: _token}, request) => {
      const {pathname} = new URL(request.url)

      if (pathname.endsWith('/sign-up/email')) {
        return void sendSignUpEmail({email: user.email, url})
      }

      if (pathname.endsWith('/change-email')) {
        // Better Auth only gives us access to the new email here.
        // But it also gives us the user id, so we can use that to get the existing email.
        const newEmail = user.email
        const userId = user.id
        const existingUser = getUser(userId)

        return void sendChangeEmailEmail({
          oldEmail: existingUser.email
          newEmail,
        })
      }
    }
  }
}
<!-- gh-comment-id:3975435786 --> @qodesmith commented on GitHub (Feb 27, 2026): For the record, I was able to mitigate against this issue by: 1. Not using `user.changeEmail.sendChangeEmailVerification` 2. Implementing conditional login inside `emailVerification.sendVerificationEmail` Example `sendVerificationEmail` implementation: ```typescript const authOptions = { emailVerification: { sendVerificationEmail: async ({user, url, token: _token}, request) => { const {pathname} = new URL(request.url) if (pathname.endsWith('/sign-up/email')) { return void sendSignUpEmail({email: user.email, url}) } if (pathname.endsWith('/change-email')) { // Better Auth only gives us access to the new email here. // But it also gives us the user id, so we can use that to get the existing email. const newEmail = user.email const userId = user.id const existingUser = getUser(userId) return void sendChangeEmailEmail({ oldEmail: existingUser.email newEmail, }) } } } } ```
Author
Owner

@steinitz commented on GitHub (Feb 28, 2026):

@qodesmith

Re your scenario question, Better Auth seems not to acknowledge your second scenario. That's the issue.

I hope that mitigation works for you. I found it was tricky to mitigate without causing other email verification issues. Moving target kind of thing.

Honestly, I've spent so much time, effort and head scratching on this I just don't have the heart to contribute much further. Even reading related code triggers some PTSD.

<!-- gh-comment-id:3975845924 --> @steinitz commented on GitHub (Feb 28, 2026): @qodesmith Re your scenario question, Better Auth seems not to acknowledge your second scenario. That's the issue. I hope that mitigation works for you. I found it was tricky to mitigate without causing other email verification issues. Moving target kind of thing. Honestly, I've spent so much time, effort and head scratching on this I just don't have the heart to contribute much further. Even reading related code triggers some PTSD.
Author
Owner

@ping-maxwell commented on GitHub (Mar 25, 2026):

Hey I believe this makes sense.

TLDR (kind of a self-note for future me to track):
Support for:

emailVerification: {
  sendOldEmailVerification: false
}

Meaning:

  • Only verify the new email
  • Skip verifying the old email

EDIT: Just realized there is a PR already open, will review it.

<!-- gh-comment-id:4128940860 --> @ping-maxwell commented on GitHub (Mar 25, 2026): Hey I believe this makes sense. TLDR (kind of a self-note for future me to track): Support for: ```ts emailVerification: { sendOldEmailVerification: false } ``` Meaning: - Only verify the new email - Skip verifying the old email --- EDIT: Just realized there is a PR already open, will review it.
Author
Owner

@ruban-s commented on GitHub (Mar 31, 2026):

Hi @ping-maxwell can i take this issue.

<!-- gh-comment-id:4162486928 --> @ruban-s commented on GitHub (Mar 31, 2026): Hi @ping-maxwell can i take this issue.
Author
Owner

@ping-maxwell commented on GitHub (Mar 31, 2026):

Hey that would be great thanks! @ruban-s

<!-- gh-comment-id:4162508342 --> @ping-maxwell commented on GitHub (Mar 31, 2026): Hey that would be great thanks! @ruban-s
Author
Owner

@ruban-s commented on GitHub (Apr 2, 2026):

I've submitted a PR that addresses this issue: #8877. Would appreciate a review when you get a chance! @ping-maxwell.

<!-- gh-comment-id:4174878823 --> @ruban-s commented on GitHub (Apr 2, 2026): I've submitted a PR that addresses this issue: #8877. Would appreciate a review when you get a chance! @ping-maxwell.
Author
Owner

@gustavovalverde commented on GitHub (Apr 14, 2026):

Related in the same area (not duplicates): #8043, #7002.

<!-- gh-comment-id:4244779559 --> @gustavovalverde commented on GitHub (Apr 14, 2026): Related in the same area (not duplicates): #8043, #7002.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18338