Email changes do not trigger cache invalidation or updates in secondary storage. #630

Closed
opened 2026-03-13 07:57:45 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @smultar on GitHub (Feb 6, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a Next.js app using the app directory with React 19.
  2. Implement authentication with MongoDB as the primary store and Redis as the secondary.
  3. Register an account through the login process.
  4. Attempt to change the account's email using sendEmailVerification.
  5. Verify the email and observe the updated user object in MongoDB.
  6. Confirm no changes are reflected in Redis.
  7. Re-login to trigger an updated user session.

Current vs. Expected behavior

Current Behavior:

  • When a user changes their email and verifies it, the user object updates in MongoDB.
  • Redis (secondary storage) does not reflect the updated information.
  • User session remains unchanged until the user manually re-logs in.

Expected Behavior:

  • After email verification, both MongoDB and Redis should reflect the updated user data.
  • The user session should automatically update without requiring a re-login.
  • Redis should either be updated or invalidated to ensure data consistency.

What version of Better Auth are you using?

1.1.16

Provide environment information

- OS: Windows 11
- Browser: Edge

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

Client, Backend

Auth config (if applicable)

import snowflake from '@/lib/snowflake';

// Databases
import mongo from '@/lib/mongo';
import redis from '@/lib/redis';

// Better Auth
import { betterAuth } from 'better-auth';
import { nextCookies } from 'better-auth/next-js';
import { mongodbAdapter } from 'better-auth/adapters/mongodb';
import { magicLink, organization } from 'better-auth/plugins';

// Email
import { Resend } from 'resend';
import { MagicLink } from '@/emails/magic-link';
import { Verify } from '@/emails/verify';

export const auth = betterAuth({
	trustedOrigins: ['app://', 'https://your-domain.com', 'http://localhost:3000'],

	database: mongodbAdapter((await mongo).db('auth')),
	secondaryStorage: {
		get: async (key) => {
			const value = await (await redis).get(`auth:${key}`);
			return value ? value : null;
		},
		set: async (key, value, ttl) => {
			if (ttl) await (await redis).set(`auth:${key}`, value, { EX: ttl });
			else await (await redis).set(`auth:${key}`, value);
		},
		delete: async (key) => {
			await (await redis).del(key);
		},
	},

	rateLimit: {
		window: 60,
		max: 100,
	},

	user: {
		changeEmail: {
			enabled: true,
			sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => {
				const resend = new Resend(process.env.RESEND_KEY);

				const ip = request?.headers.get('X-Forwarded-For') || 'unknown ip';
				const location = (request as any)?.cf ?? 'unknown location';

				const { data, error } = await resend.emails.send({
					from: 'No Reply <noreply@your-domain.com>',
					to: user.email,
					subject: 'Verify your new email address',
					react: Verify({ email: newEmail, link: url, ip, location }),
				});

				if (data) console.log(`Verification sent to ${user.email}`, data);
				if (error) console.error(`Failed to send verification to ${user.email}`, error);

				return;
			},
		},
		additionalFields: {
			sid: {
				type: 'string', unique: true, defaultValue() {
					return snowflake();
				},
			},
			handle: { type: 'string', unique: true },
			banner: { type: 'string', defaultValue: 'https://cdn.your-domain.com/images/default-banner.png' },
		},
	},

	secret: process.env.BETTER_AUTH_SECRET!,

	plugins: [
		magicLink({
			sendMagicLink: async ({ email, token, url }, request) => {
				const resend = new Resend(process.env.RESEND_KEY);

				const ip = request?.headers.get('X-Forwarded-For') || 'unknown ip';
				const location = (request as any)?.cf ?? 'unknown location';

				const { data, error } = await resend.emails.send({
					from: 'No Reply <noreply@your-domain.com>',
					to: email,
					subject: 'Secure login link',
					react: MagicLink({ email, link: url, ip, location }),
				});

				if (data) console.log(`Magic link sent to ${email}`, data);
				if (error) console.error(`Failed to send magic link to ${email}`, error);

				return;
			},
			rateLimit: {
				window: 60,
				max: 5,
			}
		}),

		organization(),
		nextCookies(),
	],

	socialProviders: {
		google: {
			clientId: process.env.AUTH_GOOGLE_ID!,
			clientSecret: process.env.AUTH_GOOGLE_SECRET!,
		},
		discord: {
			clientId: process.env.AUTH_DISCORD_ID!,
			clientSecret: process.env.AUTH_DISCORD_SECRET!,
		},
	},

	session: {
		expiresIn: 60 * 60 * 24 * 7, // 7 days
		freshAge: 60 * 10,           // 10 minutes
		updateAge: 60 * 60 * 24,     // 1 day
		cookieCache: {
			enabled: true,
			maxAge: 5 * 60,
		}
	},

	hooks: {
		before: async (ctx) => {
			if (ctx.path === '/magic-link/verify' || ctx.path === '/callback') {
				const session = ctx.context.newSession;
				if (!session) return;

				console.log('verify:', session.user.email);

				const former = await (await mongo).db('migrate').collection('users').findOne({ email: session.user.email });
				if (!former) return;

				// TODO: Add the code to migrate the user to the new database

				return;
			}
		}
	}
});

type Session = typeof auth.$Infer.Session;

Additional context

No response

Originally created by @smultar on GitHub (Feb 6, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a Next.js app using the app directory with React 19. 2. Implement authentication with MongoDB as the primary store and Redis as the secondary. 3. Register an account through the login process. 4. Attempt to change the account's email using `sendEmailVerification`. 5. Verify the email and observe the updated user object in MongoDB. 6. Confirm no changes are reflected in Redis. 7. Re-login to trigger an updated user session. ### Current vs. Expected behavior **Current Behavior:** - When a user changes their email and verifies it, the user object updates in MongoDB. - Redis (secondary storage) does **not** reflect the updated information. - User session remains unchanged until the user manually re-logs in. **Expected Behavior:** - After email verification, both MongoDB and Redis should reflect the updated user data. - The user session should automatically update without requiring a re-login. - Redis should either be updated or invalidated to ensure data consistency. ### What version of Better Auth are you using? 1.1.16 ### Provide environment information ```bash - OS: Windows 11 - Browser: Edge ``` ### Which area(s) are affected? (Select all that apply) Client, Backend ### Auth config (if applicable) ```typescript import snowflake from '@/lib/snowflake'; // Databases import mongo from '@/lib/mongo'; import redis from '@/lib/redis'; // Better Auth import { betterAuth } from 'better-auth'; import { nextCookies } from 'better-auth/next-js'; import { mongodbAdapter } from 'better-auth/adapters/mongodb'; import { magicLink, organization } from 'better-auth/plugins'; // Email import { Resend } from 'resend'; import { MagicLink } from '@/emails/magic-link'; import { Verify } from '@/emails/verify'; export const auth = betterAuth({ trustedOrigins: ['app://', 'https://your-domain.com', 'http://localhost:3000'], database: mongodbAdapter((await mongo).db('auth')), secondaryStorage: { get: async (key) => { const value = await (await redis).get(`auth:${key}`); return value ? value : null; }, set: async (key, value, ttl) => { if (ttl) await (await redis).set(`auth:${key}`, value, { EX: ttl }); else await (await redis).set(`auth:${key}`, value); }, delete: async (key) => { await (await redis).del(key); }, }, rateLimit: { window: 60, max: 100, }, user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => { const resend = new Resend(process.env.RESEND_KEY); const ip = request?.headers.get('X-Forwarded-For') || 'unknown ip'; const location = (request as any)?.cf ?? 'unknown location'; const { data, error } = await resend.emails.send({ from: 'No Reply <noreply@your-domain.com>', to: user.email, subject: 'Verify your new email address', react: Verify({ email: newEmail, link: url, ip, location }), }); if (data) console.log(`Verification sent to ${user.email}`, data); if (error) console.error(`Failed to send verification to ${user.email}`, error); return; }, }, additionalFields: { sid: { type: 'string', unique: true, defaultValue() { return snowflake(); }, }, handle: { type: 'string', unique: true }, banner: { type: 'string', defaultValue: 'https://cdn.your-domain.com/images/default-banner.png' }, }, }, secret: process.env.BETTER_AUTH_SECRET!, plugins: [ magicLink({ sendMagicLink: async ({ email, token, url }, request) => { const resend = new Resend(process.env.RESEND_KEY); const ip = request?.headers.get('X-Forwarded-For') || 'unknown ip'; const location = (request as any)?.cf ?? 'unknown location'; const { data, error } = await resend.emails.send({ from: 'No Reply <noreply@your-domain.com>', to: email, subject: 'Secure login link', react: MagicLink({ email, link: url, ip, location }), }); if (data) console.log(`Magic link sent to ${email}`, data); if (error) console.error(`Failed to send magic link to ${email}`, error); return; }, rateLimit: { window: 60, max: 5, } }), organization(), nextCookies(), ], socialProviders: { google: { clientId: process.env.AUTH_GOOGLE_ID!, clientSecret: process.env.AUTH_GOOGLE_SECRET!, }, discord: { clientId: process.env.AUTH_DISCORD_ID!, clientSecret: process.env.AUTH_DISCORD_SECRET!, }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days freshAge: 60 * 10, // 10 minutes updateAge: 60 * 60 * 24, // 1 day cookieCache: { enabled: true, maxAge: 5 * 60, } }, hooks: { before: async (ctx) => { if (ctx.path === '/magic-link/verify' || ctx.path === '/callback') { const session = ctx.context.newSession; if (!session) return; console.log('verify:', session.user.email); const former = await (await mongo).db('migrate').collection('users').findOne({ email: session.user.email }); if (!former) return; // TODO: Add the code to migrate the user to the new database return; } } } }); type Session = typeof auth.$Infer.Session; ``` ### Additional context _No response_
GiteaMirror added the bug label 2026-03-13 07:57:45 -05:00
Author
Owner

@smultar commented on GitHub (Feb 6, 2025):

Confirmed that the issue has been resolved in better-auth@1.1.17-beta.2

@smultar commented on GitHub (Feb 6, 2025): Confirmed that the issue has been resolved in `better-auth@1.1.17-beta.2`
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#630