[GH-ISSUE #1799] Email verification link after changing email of an unverified email results in ?error=user_not_found #17553

Closed
opened 2026-04-15 15:43:34 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @DevDuki on GitHub (Mar 13, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/1799

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a user via Email/Password
  2. Better Auth will send a verification email to the user
  3. DO NOT verify this user's email
  4. Change the email address to a new email address
  5. Better Auth will skip sending the approval E-Mail, because the old email is not verified anyway (expected)
  6. Better Auth will change the email (expected)
  7. Better Auth will send a verification email to the new user (expected)
  8. Click on the link in the verification email to verify the new email address
  9. Better Auth fails to verify email and adds ?error=user_not_found to the url. (unexpected)

I noticed that the payload in the JWT contains the fields email (with the old email) and updatedTo (with the new email). Perhaps this payload was meant for the approval email, but has been reused for the verification email since approval step got skipped?

Perhaps worth noting, I execute all actions on the server. Like auth.api.changeEmail or auth.api.signInEmail.

Current vs. Expected behavior

The verification of the new email fails, but I expect to be able to verify the new email after changing from an old unverified email.

(The change email flow works flawlessly when the old email is verified, so the hiccup is really only when the old email is not verified)

What version of Better Auth are you using?

1.2.4-beta.7 (I had 1.2.2 before but this one did not update the email in the session in secondary storage)

Provide environment information

- OS: Mac Sequoia 15.1
- Browser: Chrome
- Framework: SvelteKit 5

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

Backend

Auth config (if applicable)

import { route } from '$lib/routeUtils';
import { prisma } from '$lib/server/db';
import { sendEmail } from '$lib/server/email';
import { redis } from '$lib/server/redis';
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';

export const auth = betterAuth({
	// Database
	database: prismaAdapter(prisma, {
		provider: 'sqlite'
	}),

	// Email/Password
	emailAndPassword: {
		enabled: true,
	},
	emailVerification: {
		sendOnSignUp: true,
		autoSignInAfterVerification: true,
		sendVerificationEmail: async ({ user, url }) => {
			const newUrl = new URL(url);
			newUrl.searchParams.set('callbackURL', route('/(protected)'));

			await sendEmail({
				from: 'onboarding@resend.dev',
				to: user.email,
				subject: 'Verify your email address',
				text: `Click the link or copy/paste it in you browser to verify your email: ${newUrl.toString()}`,
				html: `<a href="${newUrl.toString()}">Click here to verify your email!</a>`
			});
		}
	},

	// User
	user: {
		changeEmail: {
			enabled: true,
			sendChangeEmailVerification: async ({ user, url }) => {
				await sendEmail({
					from: 'me',
					to: user.email, // verification email must be sent to the current user email to approve the change
					subject: 'Approve email change',
					text: `Click the link or copy/paste it in you browser to approve the change of your email: ${url}`,
					html: `<a href="${url}">Click here to approve the change of your email!</a>`
				})
			}
		},
	},

	// Redis
	secondaryStorage: {
		get: async (key) => {
			const value = await redis.get(REDIS_PREFIX + key);
			return value ? value : null;
		},
		set: async (key, value, ttl) => {
			if (ttl) await redis.set(REDIS_PREFIX + key, value, { EX: ttl });
			else await redis.set(REDIS_PREFIX + key, value);
		},
		delete: async (key) => {
			await redis.del(REDIS_PREFIX + key);
		}
	}
});

Additional context

Perhaps it helps when reading this conversation on discord to see where I am coming from? At first I wasn't sure if it's a bug or if I was doing something wrong, but now I am confident it could be a bug on better-auth's side.

https://discord.com/channels/1288403910284935179/1347991405926027344

Originally created by @DevDuki on GitHub (Mar 13, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/1799 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a user via Email/Password 2. Better Auth will send a verification email to the user 3. **DO NOT** verify this user's email 4. Change the email address to a new email address 5. Better Auth will skip sending the approval E-Mail, because the old email is not verified anyway (expected) 6. Better Auth will change the email (expected) 7. Better Auth will send a verification email to the new user (expected) 8. Click on the link in the verification email to verify the new email address 9. Better Auth fails to verify email and adds `?error=user_not_found` to the url. (unexpected) I noticed that the payload in the JWT contains the fields `email` (with the old email) and `updatedTo` (with the new email). Perhaps this payload was meant for the approval email, but has been reused for the verification email since approval step got skipped? Perhaps worth noting, I execute all actions on the server. Like `auth.api.changeEmail` or `auth.api.signInEmail`. ### Current vs. Expected behavior The verification of the new email fails, but I expect to be able to verify the new email after changing from an old unverified email. (The change email flow works flawlessly when the old email is verified, so the hiccup is really only when the old email is not verified) ### What version of Better Auth are you using? 1.2.4-beta.7 (I had 1.2.2 before but this one did not update the email in the session in secondary storage) ### Provide environment information ```bash - OS: Mac Sequoia 15.1 - Browser: Chrome - Framework: SvelteKit 5 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { route } from '$lib/routeUtils'; import { prisma } from '$lib/server/db'; import { sendEmail } from '$lib/server/email'; import { redis } from '$lib/server/redis'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; export const auth = betterAuth({ // Database database: prismaAdapter(prisma, { provider: 'sqlite' }), // Email/Password emailAndPassword: { enabled: true, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { const newUrl = new URL(url); newUrl.searchParams.set('callbackURL', route('/(protected)')); await sendEmail({ from: 'onboarding@resend.dev', to: user.email, subject: 'Verify your email address', text: `Click the link or copy/paste it in you browser to verify your email: ${newUrl.toString()}`, html: `<a href="${newUrl.toString()}">Click here to verify your email!</a>` }); } }, // User user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, url }) => { await sendEmail({ from: 'me', to: user.email, // verification email must be sent to the current user email to approve the change subject: 'Approve email change', text: `Click the link or copy/paste it in you browser to approve the change of your email: ${url}`, html: `<a href="${url}">Click here to approve the change of your email!</a>` }) } }, }, // Redis secondaryStorage: { get: async (key) => { const value = await redis.get(REDIS_PREFIX + key); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await redis.set(REDIS_PREFIX + key, value, { EX: ttl }); else await redis.set(REDIS_PREFIX + key, value); }, delete: async (key) => { await redis.del(REDIS_PREFIX + key); } } }); ``` ### Additional context Perhaps it helps when reading this conversation on discord to see where I am coming from? At first I wasn't sure if it's a bug or if I was doing something wrong, but now I am confident it could be a bug on better-auth's side. https://discord.com/channels/1288403910284935179/1347991405926027344
GiteaMirror added the lockedbug labels 2026-04-15 15:43:34 -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#17553