Email Update verification link #351

Closed
opened 2026-03-13 07:42:48 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @nayzflux on GitHub (Dec 6, 2024).

Describe the bug
On email update a verification link is sent to user when the user click on this link if he's not logged in the email update failed, furthermore when the user is logged in the update works but the sendVerificationEmail is triggered to the old email.

To Reproduce
Steps to reproduce the behavior:

import { db } from "@/db";
import { env } from "@/lib/env";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

export const auth = betterAuth({
  appName: "test",

  // Database
  database: drizzleAdapter(db, {
    provider: "sqlite",
    usePlural: true,
  }),

  // Client URL
  trustedOrigins: [env.CLIENT_URL],

  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,

    sendVerificationEmail: async ({ user, url }) => {
      console.log("Email verification link:", url, user.email);
    },
  },

  // User
  user: {
    changeEmail: {
      enabled: true,

      sendChangeEmailVerification: async ({ user, newEmail, url }) => {
        console.log(
          "Update email verification link:",
          url,
          user.email,
          newEmail
        );
      },
    },
  },

  // Email / Password
  emailAndPassword: {
    enabled: true,

    autoSignIn: true,

    requireEmailVerification: true,
  },
});
  1. Verify email on signup (it works and user logged in with link)

  2. Update email

  3. Click on link when not being logged (email doesnt update and user is not logged in)

  4. Click on link when being logged (email updated and user logged but sendVerificationEmail is triggered with same token as sendChangeEmailVerification)

Expected behavior
When using options autoSignInAfterVerification set to true, user should be automatically logged and the email verified when he click on the link, the same as when he click on email verification link on sign up. When the email is update, the sendVerificationEmail shouldn't be triggered as the email is already verified and updated.

Screenshots

When not logged in and clicking on link
Capture d'écran 2024-12-06 193840

Email not updated and autoSignInAfterVerification didnt works
Capture d'écran 2024-12-06 193857

When logged in and clicking on link sendVerificationEmail trigger
Capture d'écran 2024-12-06 193932

Email updated and already verified
Capture d'écran 2024-12-06 193944

Desktop (please complete the following information):

  • OS: Windows 11
  • Browser: Mozzilla Firefox
Originally created by @nayzflux on GitHub (Dec 6, 2024). **Describe the bug** On email update a verification link is sent to user when the user click on this link if he's not logged in the email update failed, furthermore when the user is logged in the update works but the sendVerificationEmail is triggered to the old email. **To Reproduce** Steps to reproduce the behavior: 1. ```ts import { db } from "@/db"; import { env } from "@/lib/env"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; export const auth = betterAuth({ appName: "test", // Database database: drizzleAdapter(db, { provider: "sqlite", usePlural: true, }), // Client URL trustedOrigins: [env.CLIENT_URL], emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { console.log("Email verification link:", url, user.email); }, }, // User user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, newEmail, url }) => { console.log( "Update email verification link:", url, user.email, newEmail ); }, }, }, // Email / Password emailAndPassword: { enabled: true, autoSignIn: true, requireEmailVerification: true, }, }); ``` 2. Verify email on signup (it works and user logged in with link) 3. Update email 4. Click on link when not being logged (email doesnt update and user is not logged in) 5. Click on link when being logged (email updated and user logged but sendVerificationEmail is triggered with same token as sendChangeEmailVerification) **Expected behavior** When using options autoSignInAfterVerification set to true, user should be automatically logged and the email verified when he click on the link, the same as when he click on email verification link on sign up. When the email is update, the sendVerificationEmail shouldn't be triggered as the email is already verified and updated. **Screenshots** When not logged in and clicking on link ![Capture d'écran 2024-12-06 193840](https://github.com/user-attachments/assets/31d048b6-cae3-46f1-ada1-2a88d5b4aee7) Email not updated and autoSignInAfterVerification didnt works ![Capture d'écran 2024-12-06 193857](https://github.com/user-attachments/assets/24df3100-8db3-42fb-928a-49853a43bcf6) When logged in and clicking on link sendVerificationEmail trigger ![Capture d'écran 2024-12-06 193932](https://github.com/user-attachments/assets/539284ca-c39d-418b-91ad-cc23225461a3) Email updated and already verified ![Capture d'écran 2024-12-06 193944](https://github.com/user-attachments/assets/dd541b15-89cf-4994-9a0e-c221c8b6c13c) **Desktop (please complete the following information):** - OS: Windows 11 - Browser: Mozzilla Firefox
Author
Owner

@Bekacru commented on GitHub (Dec 7, 2024):

To change an email, the user must be authenticated. And, they must be authenticated with the same email they are trying to change. This is to protect against account takeovers, even if an attacker manages to obtain the token before it is used.

And a verification email is sent to the new email address to make sure it can also be verified. The verification email is not sent to the old email address.

@Bekacru commented on GitHub (Dec 7, 2024): To change an email, the user must be authenticated. And, they must be authenticated with the same email they are trying to change. This is to protect against account takeovers, even if an attacker manages to obtain the token before it is used. And a verification email is sent to the new email address to make sure it can also be verified. The verification email is **not** sent to the old email address.
Author
Owner

@nayzflux commented on GitHub (Dec 7, 2024):

Ok it makes sense that user should be authenticité to prevent takeover.
But when we click and the email update link the email is already verified so there is no need to triggered a second verification furthermore the second verification dont work because email is already updated so when user click on link its display error user not found

@nayzflux commented on GitHub (Dec 7, 2024): Ok it makes sense that user should be authenticité to prevent takeover. But when we click and the email update link the email is already verified so there is no need to triggered a second verification furthermore the second verification dont work because email is already updated so when user click on link its display error user not found
Author
Owner

@Nicolab commented on GitHub (Dec 28, 2024):

For the security, it's normal for the user to be logged in.

However, when he clicks on the email change verification link, emailVerified is always set to 0 and another email (sendVerificationEmail) is sent. This shouldn't happen, as the user has already clicked on the link sent by sendChangeEmailVerification. In fact, the process is illogical for the user (since he's just clicked, he's not expecting to receive a new verification email). So he retries several times to log in and receives lots of verification emails.

If, on top of that, the user isn't smart, he won't click on the last one received... 🤪 😅

I think that once the user has clicked on the sendChangeEmailVerification link, that's enough.

On sign up and also on email change, sending the verification email each time a user tries to log in but hasn't checked his e-mail should be handled differently. Perhaps an explicit error so that the developer can propose a link to resend the verification link. This would enable a timer to be set on the front end, such as the next possible send in 60 seconds, to avoid frantic abuse.

What do you think of it? @Bekacru @nayzflux

@Nicolab commented on GitHub (Dec 28, 2024): For the security, it's normal for the user to be logged in. However, when he clicks on the email change verification link, emailVerified is always set to 0 and another email (sendVerificationEmail) is sent. This shouldn't happen, as the user has already clicked on the link sent by sendChangeEmailVerification. In fact, the process is illogical for the user (since he's just clicked, he's not expecting to receive a new verification email). So he retries several times to log in and receives lots of verification emails. If, on top of that, the user isn't smart, he won't click on the last one received... 🤪 😅 I think that once the user has clicked on the sendChangeEmailVerification link, that's enough. On sign up and also on email change, sending the verification email each time a user tries to log in but hasn't checked his e-mail should be handled differently. Perhaps an explicit error so that the developer can propose a link to resend the verification link. This would enable a timer to be set on the front end, such as the next possible send in 60 seconds, to avoid frantic abuse. What do you think of it? @Bekacru @nayzflux
Author
Owner

@nayzflux commented on GitHub (Dec 28, 2024):

I agréé with you @Nicolab , I think when the user click on email update link he should not received another verification link, but we should send email update notification to the original email address

@nayzflux commented on GitHub (Dec 28, 2024): I agréé with you @Nicolab , I think when the user click on email update link he should not received another verification link, but we should send email update notification to the original email address
Author
Owner

@ngadceser commented on GitHub (Dec 30, 2024):

Hello, This is part of the example that I wrote.
page-name: auth.ts
export const auth = betterAuth({
// database: prismaAdapter(prisma, {
// provider: 'mongodb',
// }),
// or postgresql
database: drizzleAdapter(db_supabase, {
provider: 'pg',
}),
plugins: [nextCookies(), openAPI()],
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},

page-name: actions.ts
import { eq } from 'drizzle-orm';
import { z } from 'zod';
import { user } from '@/db/schema/supabase';
import { auth } from '@/auth';
import { APIError } from 'better-auth/api';
interface SignInErrorResponse {
error: true;
errorType:
| 'no-user'
| 'email-verification'
| 'auth-error'
| 'validation-error'
| 'etc-error';
message: string;
}
type SignInResponse = void | SignInErrorResponse;

export const loginServerAction = async ({
email,
password,
}: {
email: string;
password: string;
}): Promise => {
const loginSchema = z.object({
email: z.string().email(),
password: passwordSchema,
});

const loginValidation = loginSchema.safeParse({ email, password });
if (!loginValidation.success) {
return {
error: true,
errorType: 'validation-error',
message: loginValidation.error?.issues[0]?.message ?? 'An error occurred',
};
}

// Check in DB whether the primary verification mail address exists
// or if e-mail verification is complete
try {
const responseUser = await db_supabase
.select({
id: user.id,
email: user.email,
email_verified: user.emailVerified,
})
.from(user)
.where(eq(user.email, email));

if (responseUser.length === 0) {
  return {
    error: true,
    errorType: 'no-user',
    message: 'No registered user exists.',
  };
}
if (responseUser[0].email_verified === false) {
  return {
    error: true,
    errorType: 'email-verification',
    message:
      'Please authenticate the email you registered through the link.',
  };
}
const response = await auth.api.signInEmail({
  body: {
    email,
    password,
  },
  asResponse: true,
});
console.log('response', response);

} catch (error) {
if (error instanceof APIError) {
// For unauthenticated users, the error message is as follows,
// and the e-mail is retransmitted once again.
// errorStatus FORBIDDEN
// errorMessage API Error: FORBIDDEN Email not verifie
console.log('errorStatus', error.status);
console.log('errorMessage', error.message);
}
return {
error: true,
errorType: 'etc-error',
message: 'Incorrect email or password',
};
}

Suggested by github daveycodez
I would love it if there was a property to do "sendOnSignIn: false".

For users who repeatedly attempt to sign-in without clicking
the verification link after sign-up,
the current behavior is to resend the verification email infinitely
every time a login attempt is made.

The functionality I want to implement is to simply display a message
like "Verification is not complete" instead of automatically resending the email.

const response = wait auth.api.signInEmail()
There is also a way to check in DB before doing it, but by any chance
Is there an option I might not be aware of to achieve this?
Thank you in advance!

Conclusion:
I heard that email OTP is not retransmitted.
Logging in using an OTP (One-Time Password) sent via email is suitable for websites
that require a high level of security
and is particularly useful for users who do not access the site frequently.
On the other hand, using an email link
for one-time verification followed by password login can also be effective
in certain situations.
Both methods should be chosen based on their purpose and context,
as no single approach can be deemed superior in all circumstances.

@ngadceser commented on GitHub (Dec 30, 2024): Hello, This is part of the example that I wrote. page-name: auth.ts export const auth = betterAuth({ // database: prismaAdapter(prisma, { // provider: 'mongodb', // }), // or postgresql database: drizzleAdapter(db_supabase, { provider: 'pg', }), plugins: [nextCookies(), openAPI()], emailAndPassword: { enabled: true, requireEmailVerification: true, }, --- page-name: actions.ts import { eq } from 'drizzle-orm'; import { z } from 'zod'; import { user } from '@/db/schema/supabase'; import { auth } from '@/auth'; import { APIError } from 'better-auth/api'; interface SignInErrorResponse { error: true; errorType: | 'no-user' | 'email-verification' | 'auth-error' | 'validation-error' | 'etc-error'; message: string; } type SignInResponse = void | SignInErrorResponse; export const loginServerAction = async ({ email, password, }: { email: string; password: string; }): Promise<SignInResponse> => { const loginSchema = z.object({ email: z.string().email(), password: passwordSchema, }); const loginValidation = loginSchema.safeParse({ email, password }); if (!loginValidation.success) { return { error: true, errorType: 'validation-error', message: loginValidation.error?.issues[0]?.message ?? 'An error occurred', }; } // Check in DB whether the primary verification mail address exists // or if e-mail verification is complete try { const responseUser = await db_supabase .select({ id: user.id, email: user.email, email_verified: user.emailVerified, }) .from(user) .where(eq(user.email, email)); if (responseUser.length === 0) { return { error: true, errorType: 'no-user', message: 'No registered user exists.', }; } if (responseUser[0].email_verified === false) { return { error: true, errorType: 'email-verification', message: 'Please authenticate the email you registered through the link.', }; } const response = await auth.api.signInEmail({ body: { email, password, }, asResponse: true, }); console.log('response', response); } catch (error) { if (error instanceof APIError) { // For unauthenticated users, the error message is as follows, // and the e-mail is retransmitted once again. // errorStatus FORBIDDEN // errorMessage API Error: FORBIDDEN Email not verifie console.log('errorStatus', error.status); console.log('errorMessage', error.message); } return { error: true, errorType: 'etc-error', message: 'Incorrect email or password', }; } --- Suggested by github daveycodez I would love it if there was a property to do "sendOnSignIn: false". --- For users who repeatedly attempt to sign-in without clicking the verification link after sign-up, the current behavior is to resend the verification email infinitely every time a login attempt is made. The functionality I want to implement is to simply display a message like "Verification is not complete" instead of automatically resending the email. const response = wait auth.api.signInEmail() There is also a way to check in DB before doing it, but by any chance Is there an option I might not be aware of to achieve this? Thank you in advance! --- Conclusion: I heard that email OTP is not retransmitted. Logging in using an OTP (One-Time Password) sent via email is suitable for websites that require a high level of security and is particularly useful for users who do not access the site frequently. On the other hand, using an email link for one-time verification followed by password login can also be effective in certain situations. Both methods should be chosen based on their purpose and context, as no single approach can be deemed superior in all circumstances.
Author
Owner

@ngadceser commented on GitHub (Dec 30, 2024):

I created a separate page called Please resend email for link verification manually.

@ngadceser commented on GitHub (Dec 30, 2024): I created a separate page called Please resend email for link verification manually.
Author
Owner

@Nicolab commented on GitHub (Mar 17, 2025):

Hi,

Any update for this bug?

@Nicolab commented on GitHub (Mar 17, 2025): Hi, Any update for this bug?
Author
Owner

@Bekacru commented on GitHub (Mar 17, 2025):

@Nicolab there is no way that we could tell the user has access to the new email inbox. So whenever the click the email change verification, we set emailVerified to false and send a new verification email. I'm not sure how this is a bug?

@Bekacru commented on GitHub (Mar 17, 2025): @Nicolab there is no way that we could tell the user has access to the new email inbox. So whenever the click the email change verification, we set emailVerified to `false` and send a new verification email. I'm not sure how this is a bug?
Author
Owner

@Nicolab commented on GitHub (Mar 19, 2025):

@Bekacru I thought it was a bug.

On sign up process and also on email change process, sending the verification email each time a user tries to log in (if the verification link is not clicked). It's an unusual way (lots of e-mails sent for nothing until the user goes to check his e-mails). In my opinion, it would be wise an error message like: "You must confirm your email before to log in." If necessary, we can add a button "Resend verification link".

See also https://github.com/better-auth/better-auth/issues/1050

Regarding access to the new email inbox, the old email should remain active until the user confirms his new email. It seems to me that it works like that everywhere (Google, Github, Phoenix gen.auth (to quote another authentication system), ...).

What do you think of it?

@Nicolab commented on GitHub (Mar 19, 2025): @Bekacru I thought it was a bug. On sign up process and also on email change process, sending the verification email each time a user tries to log in (if the verification link is not clicked). It's an unusual way (lots of e-mails sent for nothing until the user goes to check his e-mails). In my opinion, it would be wise an error message like: "You must confirm your email before to log in." If necessary, we can add a button "Resend verification link". See also https://github.com/better-auth/better-auth/issues/1050 Regarding access to the new email inbox, the old email should remain active until the user confirms his new email. It seems to me that it works like that everywhere (Google, Github, Phoenix gen.auth (to quote another authentication system), ...). What do you think of it?
Author
Owner

@dosubot[bot] commented on GitHub (Jun 18, 2025):

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

Issue Summary:

  • The issue involves a bug in the email update verification process.
  • Clicking the verification link without being logged in results in failure.
  • When logged in, a verification email is sent to the old address.
  • Discussion includes balancing security with user experience, with suggestions for improvements.

Next Steps:

  • Please let us know if this issue is still relevant to the latest version of the better-auth repository by commenting here.
  • If there is no further activity, this issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jun 18, 2025): Hi, @nayzflux. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog. I'm marking this issue as stale. **Issue Summary:** - The issue involves a bug in the email update verification process. - Clicking the verification link without being logged in results in failure. - When logged in, a verification email is sent to the old address. - Discussion includes balancing security with user experience, with suggestions for improvements. **Next Steps:** - Please let us know if this issue is still relevant to the latest version of the better-auth repository by commenting here. - If there is no further activity, this issue will be automatically closed in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#351