Anonymous plugin's 'after' hook incorrectly rejects successful user creation requests (Auto anonymous user sign in fails) #1591

Closed
opened 2026-03-13 08:50:20 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @ajanraj on GitHub (Jul 28, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Steps to Reproduce

Environment Setup

  • Framework: Next.js 15 with React 19
  • Database: Convex with @convex-dev/better-auth integration
  • Better Auth Version: 1.2.12
  • @convex-dev/better-auth Version: 0.7.11

1. Create Better Auth Configuration

// lib/auth.ts
import { convexAdapter } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { betterAuth } from "better-auth";
import { anonymous } from "better-auth/plugins"

export const createAuth = (ctx: GenericCtx) =>
  betterAuth({
    baseURL: process.env.SITE_URL,
    database: convexAdapter(ctx, betterAuthComponent),
    trustedOrigins: [process.env.SITE_URL as string],
    plugins: [
      convex(),
      anonymous({
        onLinkAccount: async ({ anonymousUser, newUser }) => {
          // Account linking logic here
        }
      }),
    ],
  });

2. Create Anonymous Sign-in Component

// app/components/auth/anonymous-sign-in.tsx
'use client';

import { useEffect, useRef } from 'react';
import { authClient } from '@/lib/auth-client';

export function AnonymousSignIn() {
  const attemptedAnon = useRef(false);

  useEffect(() => {
    if (!attemptedAnon.current) {
      attemptedAnon.current = true;

      const signInAnonymously = async () => {
        try {
          const result = await authClient.signIn.anonymous();
          console.log('Success:', result);
        } catch (error) {
          console.error('Error:', error);
          // Error: "Anonymous users cannot sign in again anonymously"
        }
      };

      signInAnonymously();
    }
  }, []);

  return <div>Loading...</div>;
}

3. Reproduce the Issue

  1. Open application in incognito/private browsing mode
  2. Navigate to page that renders AnonymousSignIn component
  3. Observe network request: POST /api/auth/sign-in/anonymous returns 400 Bad Request
  4. Check database: Anonymous user record was successfully created
  5. Reload the page: Application works perfectly (no new sign-in attempt)

Current vs. Expected behavior

Current Behavior:
When attempting to sign in anonymously for the first time:

  1. Server successfully creates anonymous user and session
  2. Client receives 400 Bad Request with error: "Anonymous users cannot sign in again anonymously"
  3. User creation actually succeeds (visible in database)
  4. Page reload works perfectly (because session cookie exists)

Expected Behavior:

  • Anonymous sign-in should succeed on first attempt
  • Client should receive success response and be properly authenticated
  • No page reload should be required

What version of Better Auth are you using?

1.2.12

Provide environment information

- **OS**: macOS
- **Browser**: Chrome (tested in incognito mode)
- **Package Manager**: Bun
- **Better Auth**: 1.2.12
- **@convex-dev/better-auth**: 0.7.11
- **Integration**: Convex database with Better Auth adapter

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

Client, Backend

Auth config (if applicable)

export const createAuth = (ctx: GenericCtx) =>
  betterAuth({
    baseURL: process.env.SITE_URL,
    database: convexAdapter(ctx, betterAuthComponent),
    trustedOrigins: [process.env.SITE_URL as string],
    socialProviders: {
      google: { 
        prompt: "select_account", 
        clientId: process.env.AUTH_GOOGLE_ID as string,
        clientSecret: process.env.AUTH_GOOGLE_SECRET as string,
      },
    },
    account: {
      accountLinking: {
        enabled: true, 
        allowDifferentEmails: true,
      }
    },
    plugins: [
      convex(),
      anonymous({
        onLinkAccount: async ({ anonymousUser, newUser }) => {
          // Account linking implementation
        }
      }),
    ],
  });

Additional context

Technical Root Cause Analysis

Bug Location

The issue is located in the Better Auth anonymous plugin's after hook:

  • File: packages/better-auth/src/plugins/anonymous/index.ts

Hook Execution Flow (Problematic)

// Simplified version of the problematic code
handler: createAuthMiddleware(async (ctx) => {
  const setCookie = ctx.context.responseHeaders?.get("set-cookie");
  const sessionCookie = parseSetCookieHeader(setCookie || "").get(sessionTokenName)?.value.split(".")[0];
  
  if (!sessionCookie) {
    return; // No session cookie, continue
  }
  
  const session = await getSessionFromCtx(ctx, { disableRefresh: true });
  
  if (!session || !session.user.isAnonymous) {
    return; // No existing anonymous session, continue  
  }
  
  if (ctx.path === "/sign-in/anonymous") {
    throw new APIError("BAD_REQUEST", {
      message: ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY
    });
  }
})

The Timing Bug Explained

  1. User Creation Succeeds: Anonymous sign-in endpoint successfully creates user and session
  2. Session Cookie Set: Response headers include the new session cookie
  3. After Hook Executes: The after hook runs and detects the newly created session cookie
  4. Session Detection: Hook calls getSessionFromCtx() which finds the session (just created in this request)
  5. Incorrect Rejection: Since session.user.isAnonymous is true and path is /sign-in/anonymous, the hook throws the error
  6. Request Fails: The hook rejects the very request that successfully created the anonymous user

Why Page Reload Works

  • On reload, the session cookie exists from the previous (successful) creation
  • The component detects an existing session and doesn't attempt anonymous sign-in
  • No new /sign-in/anonymous request is made

Debug Evidence

Client-Side Debug Output

[anon-1753664152484-tshj8s9c7] About to call authClient.signIn.anonymous()
POST http://localhost:3000/api/auth/sign-in/anonymous 400 (Bad Request)
Error: "Anonymous users cannot sign in again anonymously"

authClient.getSession(): {data: null, error: null}
Auth-related cookies: []

Server-Side Debug Output

🔍 SERVER DEBUG - onCreateUser called: {
  userIsAnonymous: true,
  userName: null,
  userEmail: null,
  timestamp: "2025-07-28T02:05:43.123Z",
  hasAuthIdentity: true  // ← Shows server detects auth identity
}

Key Insight: Server successfully creates the user (onCreateUser called) but client receives error response due to the hook's timing issue.

Originally created by @ajanraj on GitHub (Jul 28, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ## Steps to Reproduce ### Environment Setup - **Framework**: Next.js 15 with React 19 - **Database**: Convex with @convex-dev/better-auth integration - **Better Auth Version**: 1.2.12 - **@convex-dev/better-auth Version**: 0.7.11 ### 1. Create Better Auth Configuration ```typescript // lib/auth.ts import { convexAdapter } from "@convex-dev/better-auth"; import { convex } from "@convex-dev/better-auth/plugins"; import { betterAuth } from "better-auth"; import { anonymous } from "better-auth/plugins" export const createAuth = (ctx: GenericCtx) => betterAuth({ baseURL: process.env.SITE_URL, database: convexAdapter(ctx, betterAuthComponent), trustedOrigins: [process.env.SITE_URL as string], plugins: [ convex(), anonymous({ onLinkAccount: async ({ anonymousUser, newUser }) => { // Account linking logic here } }), ], }); ``` ### 2. Create Anonymous Sign-in Component ```typescript // app/components/auth/anonymous-sign-in.tsx 'use client'; import { useEffect, useRef } from 'react'; import { authClient } from '@/lib/auth-client'; export function AnonymousSignIn() { const attemptedAnon = useRef(false); useEffect(() => { if (!attemptedAnon.current) { attemptedAnon.current = true; const signInAnonymously = async () => { try { const result = await authClient.signIn.anonymous(); console.log('Success:', result); } catch (error) { console.error('Error:', error); // Error: "Anonymous users cannot sign in again anonymously" } }; signInAnonymously(); } }, []); return <div>Loading...</div>; } ``` ### 3. Reproduce the Issue 1. Open application in incognito/private browsing mode 2. Navigate to page that renders `AnonymousSignIn` component 3. Observe network request: `POST /api/auth/sign-in/anonymous` returns `400 Bad Request` 4. Check database: Anonymous user record was successfully created 5. Reload the page: Application works perfectly (no new sign-in attempt) ### Current vs. Expected behavior **Current Behavior:** When attempting to sign in anonymously for the first time: 1. Server successfully creates anonymous user and session 2. Client receives `400 Bad Request` with error: "Anonymous users cannot sign in again anonymously" 3. User creation actually succeeds (visible in database) 4. Page reload works perfectly (because session cookie exists) **Expected Behavior:** - Anonymous sign-in should succeed on first attempt - Client should receive success response and be properly authenticated - No page reload should be required ### What version of Better Auth are you using? 1.2.12 ### Provide environment information ```bash - **OS**: macOS - **Browser**: Chrome (tested in incognito mode) - **Package Manager**: Bun - **Better Auth**: 1.2.12 - **@convex-dev/better-auth**: 0.7.11 - **Integration**: Convex database with Better Auth adapter ``` ### Which area(s) are affected? (Select all that apply) Client, Backend ### Auth config (if applicable) ```typescript export const createAuth = (ctx: GenericCtx) => betterAuth({ baseURL: process.env.SITE_URL, database: convexAdapter(ctx, betterAuthComponent), trustedOrigins: [process.env.SITE_URL as string], socialProviders: { google: { prompt: "select_account", clientId: process.env.AUTH_GOOGLE_ID as string, clientSecret: process.env.AUTH_GOOGLE_SECRET as string, }, }, account: { accountLinking: { enabled: true, allowDifferentEmails: true, } }, plugins: [ convex(), anonymous({ onLinkAccount: async ({ anonymousUser, newUser }) => { // Account linking implementation } }), ], }); ``` ### Additional context ## Technical Root Cause Analysis ### Bug Location The issue is located in the Better Auth anonymous plugin's `after` hook: - **File**: `packages/better-auth/src/plugins/anonymous/index.ts` ### Hook Execution Flow (Problematic) ```javascript // Simplified version of the problematic code handler: createAuthMiddleware(async (ctx) => { const setCookie = ctx.context.responseHeaders?.get("set-cookie"); const sessionCookie = parseSetCookieHeader(setCookie || "").get(sessionTokenName)?.value.split(".")[0]; if (!sessionCookie) { return; // No session cookie, continue } const session = await getSessionFromCtx(ctx, { disableRefresh: true }); if (!session || !session.user.isAnonymous) { return; // No existing anonymous session, continue } if (ctx.path === "/sign-in/anonymous") { throw new APIError("BAD_REQUEST", { message: ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY }); } }) ``` ### The Timing Bug Explained 1. **User Creation Succeeds**: Anonymous sign-in endpoint successfully creates user and session 2. **Session Cookie Set**: Response headers include the new session cookie 3. **After Hook Executes**: The `after` hook runs and detects the newly created session cookie 4. **Session Detection**: Hook calls `getSessionFromCtx()` which finds the session (just created in this request) 5. **Incorrect Rejection**: Since `session.user.isAnonymous` is true and path is `/sign-in/anonymous`, the hook throws the error 6. **Request Fails**: The hook rejects the very request that successfully created the anonymous user ### Why Page Reload Works - On reload, the session cookie exists from the previous (successful) creation - The component detects an existing session and doesn't attempt anonymous sign-in - No new `/sign-in/anonymous` request is made ## Debug Evidence ### Client-Side Debug Output ``` [anon-1753664152484-tshj8s9c7] About to call authClient.signIn.anonymous() POST http://localhost:3000/api/auth/sign-in/anonymous 400 (Bad Request) Error: "Anonymous users cannot sign in again anonymously" authClient.getSession(): {data: null, error: null} Auth-related cookies: [] ``` ### Server-Side Debug Output ``` 🔍 SERVER DEBUG - onCreateUser called: { userIsAnonymous: true, userName: null, userEmail: null, timestamp: "2025-07-28T02:05:43.123Z", hasAuthIdentity: true // ← Shows server detects auth identity } ``` **Key Insight**: Server successfully creates the user (`onCreateUser` called) but client receives error response due to the hook's timing issue.
GiteaMirror added the bug label 2026-03-13 08:50:20 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 28, 2025):

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

You’ve correctly identified a timing bug in the anonymous plugin’s after hook: after a successful anonymous sign-in, the hook inspects the session cookie set by the current request and mistakenly treats it as a repeat sign-in, returning a 400 error—even though the user and session were just created. This happens because the hook checks for any anonymous session after the handler, without distinguishing between a session created by this request and one that existed before. The code currently does not use ctx.context.newSession, which is available in the after hook specifically to identify sessions created during the current request, and instead relies on parsing cookies and fetching the session again, leading to this false positive see code.

Workaround suggestion:
Modify the after hook logic to check for ctx.context.newSession before throwing the error. If ctx.context.newSession exists and is anonymous, it means the session was just created and the request should not be rejected. Here’s a pseudocode outline for the fix:

handler: createAuthMiddleware(async (ctx) => {
  // ...existing code to get sessionCookie and session...

  // Only throw if there is an existing anonymous session and this is NOT a new session created by this request
  if (
    ctx.path === "/sign-in/anonymous" &&
    session?.user.isAnonymous &&
    !ctx.context.newSession // <-- Only throw if not a new session
  ) {
    throw new APIError("BAD_REQUEST", {
      message: ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY,
    });
  }
  // ...rest of the hook...
});

This change ensures that the error is only thrown for actual repeat sign-in attempts, not for the initial successful sign-in. There are no official workarounds or fixes documented yet, and no open issues or merged PRs address this bug docs.

Let me know if you want a more detailed patch or have questions about integrating this workaround.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Jul 28, 2025): <!-- Greeting --> Hi @ajanraj! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> You’ve correctly identified a timing bug in the anonymous plugin’s `after` hook: after a successful anonymous sign-in, the hook inspects the session cookie set by the current request and mistakenly treats it as a repeat sign-in, returning a 400 error—even though the user and session were just created. This happens because the hook checks for any anonymous session after the handler, without distinguishing between a session created by this request and one that existed before. The code currently does not use `ctx.context.newSession`, which is available in the `after` hook specifically to identify sessions created during the current request, and instead relies on parsing cookies and fetching the session again, leading to this false positive [see code](https://github.com/better-auth/better-auth/blob/76cac0eaa6429059f671333ea3bfb21e5ddbba5c/packages/better-auth/src/plugins/anonymous/index.ts). **Workaround suggestion:** Modify the `after` hook logic to check for `ctx.context.newSession` before throwing the error. If `ctx.context.newSession` exists and is anonymous, it means the session was just created and the request should not be rejected. Here’s a pseudocode outline for the fix: ```typescript handler: createAuthMiddleware(async (ctx) => { // ...existing code to get sessionCookie and session... // Only throw if there is an existing anonymous session and this is NOT a new session created by this request if ( ctx.path === "/sign-in/anonymous" && session?.user.isAnonymous && !ctx.context.newSession // <-- Only throw if not a new session ) { throw new APIError("BAD_REQUEST", { message: ERROR_CODES.ANONYMOUS_USERS_CANNOT_SIGN_IN_AGAIN_ANONYMOUSLY, }); } // ...rest of the hook... }); ``` This change ensures that the error is only thrown for actual repeat sign-in attempts, not for the initial successful sign-in. There are no official workarounds or fixes documented yet, and no open issues or merged PRs address this bug [docs](https://better-auth.com/docs/concepts/hooks). Let me know if you want a more detailed patch or have questions about integrating this workaround. <!-- 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/00285092-3312-492e-98e1-9fcb5bab581a?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/00285092-3312-492e-98e1-9fcb5bab581a?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/00285092-3312-492e-98e1-9fcb5bab581a?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/00285092-3312-492e-98e1-9fcb5bab581a?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/00285092-3312-492e-98e1-9fcb5bab581a?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/00285092-3312-492e-98e1-9fcb5bab581a?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/00285092-3312-492e-98e1-9fcb5bab581a?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/3658)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1591