[GH-ISSUE #4721] Email verification status not updated in secondary storage after verification #27358

Closed
opened 2026-04-17 18:19:22 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @phuctm97 on GitHub (Sep 17, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4721

Description

When secondary storage is configured and used for session management (which is the default behavior when secondary storage is configured), the emailVerified field is not automatically updated in the cached session after successful email verification. This causes getSession() to continue returning emailVerified: false until the session is refreshed.

Current Behavior

  1. User registers and receives verification email
  2. Secondary storage caches the session with emailVerified: false
  3. User clicks verification link and successfully verifies their email
  4. Database is updated with emailVerified: true
  5. Bug: Secondary storage still contains the old session with emailVerified: false
  6. Subsequent calls to getSession() return stale data from secondary storage showing emailVerified: false
  7. Session only updates after manual refresh or TTL expiration

Expected Behavior

After successful email verification:

  • The session in secondary storage should be automatically updated with emailVerified: true
  • getSession() should immediately return the updated verification status without requiring manual refresh or TTL expiration

Steps to Reproduce

  1. Configure better-auth with secondary storage (e.g., Redis):
const auth = betterAuth({
  database: /* database config */,
  secondaryStorage: {
    get: async (key) => /* Redis get */,
    set: async (key, value, ttl) => /* Redis set */,
    delete: async (key) => /* Redis del */
  },
  emailVerification: {
    sendVerificationEmail: async ({ user, url }) => {
      // Send email implementation
    }
  }
});
  1. Register a new user
  2. Verify the email using the verification link
  3. Call getSession() - observe it still returns emailVerified: false
  4. Database has correct emailVerified: true, but secondary storage still has emailVerified: false

Environment

  • better-auth version: 1.3.11
  • Secondary storage: Redis
  • Framework: React Router v7 (SPA)
  • Node.js version: 22.x

Proposed Solution

When email verification is successful, the system should:

  1. Update the database (current behavior)
  2. Identify all sessions for the verified user
  3. Update or invalidate those sessions in secondary storage to ensure consistency

Impact

This bug affects user experience as verified users may still see "Please verify your email" messages and be blocked from accessing features that require email verification, even after successfully verifying their email address.

Originally created by @phuctm97 on GitHub (Sep 17, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4721 ## Description When secondary storage is configured and used for session management (which is the default behavior when secondary storage is configured), the `emailVerified` field is not automatically updated in the cached session after successful email verification. This causes `getSession()` to continue returning `emailVerified: false` until the session is refreshed. ## Current Behavior 1. User registers and receives verification email 2. Secondary storage caches the session with `emailVerified: false` 3. User clicks verification link and successfully verifies their email 4. Database is updated with `emailVerified: true` 5. **Bug**: Secondary storage still contains the old session with `emailVerified: false` 6. Subsequent calls to `getSession()` return stale data from secondary storage showing `emailVerified: false` 7. Session only updates after manual refresh or TTL expiration ## Expected Behavior After successful email verification: - The session in secondary storage should be automatically updated with `emailVerified: true` - `getSession()` should immediately return the updated verification status without requiring manual refresh or TTL expiration ## Steps to Reproduce 1. Configure better-auth with secondary storage (e.g., Redis): ```typescript const auth = betterAuth({ database: /* database config */, secondaryStorage: { get: async (key) => /* Redis get */, set: async (key, value, ttl) => /* Redis set */, delete: async (key) => /* Redis del */ }, emailVerification: { sendVerificationEmail: async ({ user, url }) => { // Send email implementation } } }); ``` 2. Register a new user 3. Verify the email using the verification link 4. Call `getSession()` - observe it still returns `emailVerified: false` 5. Database has correct `emailVerified: true`, but secondary storage still has `emailVerified: false` ## Environment - better-auth version: 1.3.11 - Secondary storage: Redis - Framework: React Router v7 (SPA) - Node.js version: 22.x ## Proposed Solution When email verification is successful, the system should: 1. Update the database (current behavior) 2. Identify all sessions for the verified user 3. Update or invalidate those sessions in secondary storage to ensure consistency ## Impact This bug affects user experience as verified users may still see "Please verify your email" messages and be blocked from accessing features that require email verification, even after successfully verifying their email address.
GiteaMirror added the lockedbug labels 2026-04-17 18:19:22 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 17, 2025):

This is a confirmed bug in better-auth: after email verification, the emailVerified field is updated in the database, but existing sessions in secondary storage (like Redis) are not updated or invalidated, so getSession() continues to return stale data until the session is manually refreshed or expires. This happens because session data in secondary storage is only updated during authentication events (like sign-in or token refresh), not when user profile fields such as emailVerified change. The maintainers have acknowledged the issue and a PR to fix it is in progress, but as of now, it is not resolved in the latest stable release (discussion).

Until the fix is released, the recommended workaround is to manually invalidate or update sessions in secondary storage after email verification. You can do this by using a custom hook (like afterProfileUpdate) or by adding logic to your verification flow to delete or refresh all sessions for the user in secondary storage, forcing new sessions to be created with up-to-date profile info (discussion). If you want a code example for a manual session invalidation hook, let me know!

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:3302815667 --> @dosubot[bot] commented on GitHub (Sep 17, 2025): <!-- Answer --> This is a confirmed bug in better-auth: after email verification, the emailVerified field is updated in the database, but existing sessions in secondary storage (like Redis) are not updated or invalidated, so getSession() continues to return stale data until the session is manually refreshed or expires. This happens because session data in secondary storage is only updated during authentication events (like sign-in or token refresh), not when user profile fields such as emailVerified change. The maintainers have acknowledged the issue and a PR to fix it is in progress, but as of now, it is not resolved in the latest stable release ([discussion](https://github.com/better-auth/better-auth/discussions/4495)). Until the fix is released, the recommended workaround is to manually invalidate or update sessions in secondary storage after email verification. You can do this by using a custom hook (like afterProfileUpdate) or by adding logic to your verification flow to delete or refresh all sessions for the user in secondary storage, forcing new sessions to be created with up-to-date profile info ([discussion](https://github.com/better-auth/better-auth/discussions/4495)). If you want a code example for a manual session invalidation hook, let me know! <!-- 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/0f75412c-b42a-447f-b248-ea4ab39b8337?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/0f75412c-b42a-447f-b248-ea4ab39b8337?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/0f75412c-b42a-447f-b248-ea4ab39b8337?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/0f75412c-b42a-447f-b248-ea4ab39b8337?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/0f75412c-b42a-447f-b248-ea4ab39b8337?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/0f75412c-b42a-447f-b248-ea4ab39b8337?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/0f75412c-b42a-447f-b248-ea4ab39b8337?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/4721)
Author
Owner

@bpleco commented on GitHub (Oct 9, 2025):

I was having the same issue so putting this here for anyone else that lands here. Not sure if this is the cleanest solution but heres a work around until this is fixed. I'm using NuxtHub but you could update this to use redis ect.

Just pass in the updated user from the db in your hook

// might need to update your types if you've extended the user schema
import type { Session, User } from "better-auth/types";

export async function updateUserSessionInKv(updatedUser: User) {
  const kv = hubKV();

  // my kv is namespaced with _auth:
  const sessionList = await kv.get(`_auth:active-sessions-${updatedUser.id}`) as { token: string; expiresAt: number }[];

  if (!sessionList || !Array.isArray(sessionList) || sessionList.length === 0) {
    return;
  }

  await Promise.all(sessionList.map(async (session) => {
    const sessionData = await kv.get(`_auth:${session.token}`) as { session: Session; user: User }; // <- not sure if there is a type for this

    if (!sessionData)
      return;

    await kv.set(`_auth:${session.token}`, {
      session: sessionData.session,
      user: updatedUser,
    });
  }));
}

<!-- gh-comment-id:3384941421 --> @bpleco commented on GitHub (Oct 9, 2025): I was having the same issue so putting this here for anyone else that lands here. Not sure if this is the cleanest solution but heres a work around until this is fixed. I'm using NuxtHub but you could update this to use redis ect. Just pass in the updated user from the db in your hook ```ts // might need to update your types if you've extended the user schema import type { Session, User } from "better-auth/types"; export async function updateUserSessionInKv(updatedUser: User) { const kv = hubKV(); // my kv is namespaced with _auth: const sessionList = await kv.get(`_auth:active-sessions-${updatedUser.id}`) as { token: string; expiresAt: number }[]; if (!sessionList || !Array.isArray(sessionList) || sessionList.length === 0) { return; } await Promise.all(sessionList.map(async (session) => { const sessionData = await kv.get(`_auth:${session.token}`) as { session: Session; user: User }; // <- not sure if there is a type for this if (!sessionData) return; await kv.set(`_auth:${session.token}`, { session: sessionData.session, user: updatedUser, }); })); } ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27358