[GH-ISSUE #5611] Cross-Subdomain Cookie Issue: Tokens Set Then Immediately Removed After Sign-In (prod/staging) #10300

Closed
opened 2026-04-13 06:20:28 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @Rafter97 on GitHub (Oct 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5611

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Steps to Reproduce

  • Deploy API to api.example.com and frontend to example.com
  • Configure better-auth with the attached auth.ts configuration
  • Attempt to sign in using email/password
  • Observe cookies in browser DevTools during redirect from /sign-in to /

Current vs. Expected behavior

Expected Behavior:

  • Cookies should persist after sign-in
  • User should remain authenticated after redirect
  • Session should be maintained across requests

Actual Behavior:

  • API accepts sign-in request successfully
  • Cookies appear briefly in browser DevTools (visible for a couple of seconds, look at vid)
  • Cookies are automatically removed
  • User ends up unauthenticated on the redirect target page (/)

https://github.com/user-attachments/assets/66d0ca13-a00d-42ca-a889-3629ee612aef

What version of Better Auth are you using?

1.3.27

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8132",
    "release": "24.6.0",
    "cpuCount": 10,
    "cpuModel": "Apple M4",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.12 GB"
  },
  "node": {
    "version": "v22.14.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.4.2"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "^1.3.27",
    "config": null
  }
}

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

Client

Auth config (if applicable)

export function initAuth(options: {
  baseApiUrl: string;
  secret: string;
  rootDomain: string;
  socialProviders: {
    google: {
      clientId: string;
      clientSecret: string;
    };
    facebook: {
      clientId: string;
      clientSecret: string;
    };
    microsoft: {
      clientId: string;
      clientSecret: string;
    };
    apple: {
      clientId: string;
      clientSecret: string;
      appBundleIdentifier: string;
    };
  };

  onSessionCreated: (userId: string) => Promise<void>;

  appName: string;
  trustedOrigins: string[];

  sendPasswordResetEmail: (data: {
    userId: string;
    email: string;
    url: string;
    token: string;
    name: string;
  }) => Promise<void>;
}): ReturnType<typeof betterAuth> {
  const config = {
    appName: options.appName,
    baseURL: options.baseApiUrl,
    secret: options.secret,

    database: drizzleAdapter(db, {
      provider: "pg",
      schema: {
        users: users,
        sessions: sessions,
        accounts: accounts,
        verifications: verification,
        jwkss: jwks,
      },
      usePlural: true,
    }),

    advanced: {
      ipAddress: {
        ipAddressHeaders: ["x-client-ip", "x-forwarded-for"],
        disableIpTracking: false,
      },

      cookiePrefix: options.appName.toLowerCase(),

      // TODO: FIX THIS
      // crossSubDomainCookies: {
      //   enabled: true,
      //   domain: options.rootDomain,
      // },
    },

    // https://www.better-auth.com/docs/concepts/database#extending-core-schema
    user: {
      additionalFields: {
        firstName: {
          type: "string",
          required: true,
        },
        lastName: {
          type: "string",
          required: true,
        },
        lastLoggedInAt: {
          type: "date",
          required: true,
          defaultValue: new Date(),
          input: false,
        },
        blockedAt: {
          type: "date",
          required: false,
          input: false,
        },
        status: {
          type: "string",
          required: true,
          input: false,
          defaultValue: "active",
        },
        srgRestricted: {
          type: "boolean",
          defaultValue: false,
          input: false,
        },
        isTestAccount: {
          type: "boolean",
          input: false,
          defaultValue: false,
        },
        onBoardedAt: {
          type: "date",
          required: false,
          input: false,
        },
      },
    },

    trustedOrigins: options.trustedOrigins,

    plugins: [
      // https://www.better-auth.com/docs/plugins/phone-number
      phoneNumber({
        sendOTP: async ({ phoneNumber, code }) => {
          console.log("🚀 ~ phoneNumber, code:", phoneNumber, code);
          try {
            await createVerification(phoneNumber, code);
          } catch (e) {
            if (e instanceof Error) {
              console.error("🚀 ~ Error:", e.message);
            } else {
              console.error("🚀 ~ Unknown error:", e);
            }
          }
        },
      }),

      // https://www.better-auth.com/docs/plugins/jwt
      jwt({
        jwt: {
          definePayload: (ctx) => {
            return {
              id: ctx.user.id,
              name: ctx.user.name,
              sessionId: ctx.session.id,
            };
          },
        },
      }),

      // https://www.better-auth.com/docs/integrations/expo#add-the-expo-plugin-on-your-server
      // expo(),

      // https://www.better-auth.com/docs/integrations/tanstack#usage-tips
      reactStartCookies(), // make sure this is the last plugin in the array
    ],

    // https://www.better-auth.com/docs/concepts/session-management#session-caching
    session: {
      cookieCache: {
        enabled: true,
        maxAge: 5 * 60, // 5 minutes
      },
    },

    emailVerification: {
      // https://www.better-auth.com/docs/authentication/email-password#email-verification
      // eslint-disable-next-line @typescript-eslint/require-await
      sendVerificationEmail: async (data) => {
        console.log("🚀 ~ sendVerificationEmail: ~ data:", data);
        // use an email provider to send email to the user
      },
    },

    // https://www.better-auth.com/docs/authentication/email-password
    emailAndPassword: {
      enabled: true,
      autoSignIn: true,
      // https://forloopconsulting.atlassian.net/wiki/spaces/CR/pages/981499924/Customer+Communication#Subject%3A-Reset-Your-Reel-Money-Password
      resetPasswordTokenExpiresIn: 1800, // 30 mins

      sendResetPassword: async (data) => {
        await options.sendPasswordResetEmail({
          url: data.url,
          token: data.token,

          userId: data.user.id,
          name: data.user.name,
          email: data.user.email,
        });
      },
    },
  } satisfies BetterAuthOptions;

  return betterAuth(config);
}

Additional context

I found a similar issue here that didn't go anywhere: https://github.com/better-auth/better-auth/issues/3938

Attempted Solutions:
Tried enabling crossSubDomainCookies as documented:

advanced: {
  crossSubDomainCookies: {
    enabled: true,
    domain: "example.com", // root domain 
  },
}

same issue as before where token is not being set

sign-in function

const onSubmit = async (data: z.infer<typeof loginSchema>) => {
  const REDIRECT_URL: FileRouteTypes["to"] = "/"
  await authClient.signIn.email(
    {
      email: data.email,
      password: data.password,
      rememberMe: data.remember,
      callbackURL: REDIRECT_URL,
    },
    {
      onError: (ctx) => {
        if (ctx.error.status === 403) {
          toast.error("Error", {
            description: "Please verify your email address",
          });
        }

        toast.error("Error", {
          description: ctx.error.message,
        });
      },
      onSuccess: async () => {
        await queryClient.invalidateQueries({
          queryKey: authQueryOptions().queryKey,
        });
      },
    },
  );
};
Originally created by @Rafter97 on GitHub (Oct 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5611 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Steps to Reproduce - Deploy API to api.example.com and frontend to example.com - Configure better-auth with the attached auth.ts configuration - Attempt to sign in using email/password - Observe cookies in browser DevTools during redirect from `/sign-in` to `/` ### Current vs. Expected behavior Expected Behavior: - Cookies should persist after sign-in - User should remain authenticated after redirect - Session should be maintained across requests Actual Behavior: - API accepts sign-in request successfully - Cookies appear briefly in browser DevTools (visible for a couple of seconds, look at vid) - Cookies are automatically removed - User ends up unauthenticated on the redirect target page (/) https://github.com/user-attachments/assets/66d0ca13-a00d-42ca-a889-3629ee612aef ### What version of Better Auth are you using? 1.3.27 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8132", "release": "24.6.0", "cpuCount": 10, "cpuModel": "Apple M4", "totalMemory": "32.00 GB", "freeMemory": "0.12 GB" }, "node": { "version": "v22.14.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.4.2" }, "frameworks": null, "databases": null, "betterAuth": { "version": "^1.3.27", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript export function initAuth(options: { baseApiUrl: string; secret: string; rootDomain: string; socialProviders: { google: { clientId: string; clientSecret: string; }; facebook: { clientId: string; clientSecret: string; }; microsoft: { clientId: string; clientSecret: string; }; apple: { clientId: string; clientSecret: string; appBundleIdentifier: string; }; }; onSessionCreated: (userId: string) => Promise<void>; appName: string; trustedOrigins: string[]; sendPasswordResetEmail: (data: { userId: string; email: string; url: string; token: string; name: string; }) => Promise<void>; }): ReturnType<typeof betterAuth> { const config = { appName: options.appName, baseURL: options.baseApiUrl, secret: options.secret, database: drizzleAdapter(db, { provider: "pg", schema: { users: users, sessions: sessions, accounts: accounts, verifications: verification, jwkss: jwks, }, usePlural: true, }), advanced: { ipAddress: { ipAddressHeaders: ["x-client-ip", "x-forwarded-for"], disableIpTracking: false, }, cookiePrefix: options.appName.toLowerCase(), // TODO: FIX THIS // crossSubDomainCookies: { // enabled: true, // domain: options.rootDomain, // }, }, // https://www.better-auth.com/docs/concepts/database#extending-core-schema user: { additionalFields: { firstName: { type: "string", required: true, }, lastName: { type: "string", required: true, }, lastLoggedInAt: { type: "date", required: true, defaultValue: new Date(), input: false, }, blockedAt: { type: "date", required: false, input: false, }, status: { type: "string", required: true, input: false, defaultValue: "active", }, srgRestricted: { type: "boolean", defaultValue: false, input: false, }, isTestAccount: { type: "boolean", input: false, defaultValue: false, }, onBoardedAt: { type: "date", required: false, input: false, }, }, }, trustedOrigins: options.trustedOrigins, plugins: [ // https://www.better-auth.com/docs/plugins/phone-number phoneNumber({ sendOTP: async ({ phoneNumber, code }) => { console.log("🚀 ~ phoneNumber, code:", phoneNumber, code); try { await createVerification(phoneNumber, code); } catch (e) { if (e instanceof Error) { console.error("🚀 ~ Error:", e.message); } else { console.error("🚀 ~ Unknown error:", e); } } }, }), // https://www.better-auth.com/docs/plugins/jwt jwt({ jwt: { definePayload: (ctx) => { return { id: ctx.user.id, name: ctx.user.name, sessionId: ctx.session.id, }; }, }, }), // https://www.better-auth.com/docs/integrations/expo#add-the-expo-plugin-on-your-server // expo(), // https://www.better-auth.com/docs/integrations/tanstack#usage-tips reactStartCookies(), // make sure this is the last plugin in the array ], // https://www.better-auth.com/docs/concepts/session-management#session-caching session: { cookieCache: { enabled: true, maxAge: 5 * 60, // 5 minutes }, }, emailVerification: { // https://www.better-auth.com/docs/authentication/email-password#email-verification // eslint-disable-next-line @typescript-eslint/require-await sendVerificationEmail: async (data) => { console.log("🚀 ~ sendVerificationEmail: ~ data:", data); // use an email provider to send email to the user }, }, // https://www.better-auth.com/docs/authentication/email-password emailAndPassword: { enabled: true, autoSignIn: true, // https://forloopconsulting.atlassian.net/wiki/spaces/CR/pages/981499924/Customer+Communication#Subject%3A-Reset-Your-Reel-Money-Password resetPasswordTokenExpiresIn: 1800, // 30 mins sendResetPassword: async (data) => { await options.sendPasswordResetEmail({ url: data.url, token: data.token, userId: data.user.id, name: data.user.name, email: data.user.email, }); }, }, } satisfies BetterAuthOptions; return betterAuth(config); } ``` ### Additional context I found a similar issue here that didn't go anywhere: https://github.com/better-auth/better-auth/issues/3938 Attempted Solutions: Tried enabling crossSubDomainCookies as documented: ```ts advanced: { crossSubDomainCookies: { enabled: true, domain: "example.com", // root domain }, } ``` same issue as before where token is not being set sign-in function ```ts const onSubmit = async (data: z.infer<typeof loginSchema>) => { const REDIRECT_URL: FileRouteTypes["to"] = "/" await authClient.signIn.email( { email: data.email, password: data.password, rememberMe: data.remember, callbackURL: REDIRECT_URL, }, { onError: (ctx) => { if (ctx.error.status === 403) { toast.error("Error", { description: "Please verify your email address", }); } toast.error("Error", { description: ctx.error.message, }); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: authQueryOptions().queryKey, }); }, }, ); }; ```
GiteaMirror added the lockedbug labels 2026-04-13 06:20:28 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 27, 2025):

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

This is a common issue in cross-subdomain setups and is usually caused by cookie attribute misconfiguration or environmental factors. Here’s what you should check and update:

  1. Cookie Domain: The domain in crossSubDomainCookies must be set to your root domain with a leading dot, e.g., .example.com (not just example.com and never with a protocol). This ensures cookies are shared across all subdomains, including api.example.com and example.com reference.

  2. Cookie Attributes: For cross-subdomain cookies to persist, set sameSite: "none", secure: true, and httpOnly: true in your Better Auth config. Example:

    advanced: {
      crossSubDomainCookies: {
        enabled: true,
        domain: '.example.com',
      },
      defaultCookieAttributes: {
        sameSite: 'none',
        secure: true,
        httpOnly: true,
      },
    }
    

    This is required for modern browsers to accept cookies after redirects and across subdomains reference.

  3. CORS and Credentials: Your frontend must use credentials: 'include' in all fetch/auth requests, and your backend must send Access-Control-Allow-Origin (matching the frontend origin exactly) and Access-Control-Allow-Credentials: true on all responses, including OPTIONS preflight reference.

  4. HTTPS Everywhere: Both frontend and backend must be served over HTTPS. Secure cookies will not work over HTTP.

  5. Trusted Origins: Make sure every possible frontend origin (including protocol and port) is listed in trustedOrigins at the top level of your config reference.

  6. Proxy/CDN: If you use Cloudflare, Vercel, or any proxy/CDN, ensure it is not stripping or rewriting Set-Cookie headers.

  7. Browser Testing: Some browsers (Safari, Brave, Firefox) block third-party cookies by default. Test in Chrome incognito to rule out browser-side issues.

  8. Session Validation: If the session is created without a valid user id (e.g., due to a misconfigured OAuth provider or custom user mapping), Better Auth will erase the cookie immediately after login reference.

If you apply these changes and still see the issue, inspect the actual Set-Cookie headers in your network tab after sign-in and after redirect. The cookie should have the correct domain, path /, SameSite=None, and Secure attributes. If it’s missing or being deleted, it’s almost always a config or environment mismatch.

Let me know if you want a checklist or want to debug specific headers!

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:3451183653 --> @dosubot[bot] commented on GitHub (Oct 27, 2025): <!-- Greeting --> Hi @Rafter97! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a common issue in cross-subdomain setups and is usually caused by cookie attribute misconfiguration or environmental factors. Here’s what you should check and update: 1. **Cookie Domain**: The domain in `crossSubDomainCookies` must be set to your root domain with a leading dot, e.g., `.example.com` (not just `example.com` and never with a protocol). This ensures cookies are shared across all subdomains, including `api.example.com` and `example.com` [reference](https://github.com/better-auth/better-auth/issues/4483). 2. **Cookie Attributes**: For cross-subdomain cookies to persist, set `sameSite: "none"`, `secure: true`, and `httpOnly: true` in your Better Auth config. Example: ```ts advanced: { crossSubDomainCookies: { enabled: true, domain: '.example.com', }, defaultCookieAttributes: { sameSite: 'none', secure: true, httpOnly: true, }, } ``` This is required for modern browsers to accept cookies after redirects and across subdomains [reference](https://github.com/better-auth/better-auth/issues/3470). 3. **CORS and Credentials**: Your frontend must use `credentials: 'include'` in all fetch/auth requests, and your backend must send `Access-Control-Allow-Origin` (matching the frontend origin exactly) and `Access-Control-Allow-Credentials: true` on all responses, including OPTIONS preflight [reference](https://github.com/better-auth/better-auth/issues/4720). 4. **HTTPS Everywhere**: Both frontend and backend must be served over HTTPS. Secure cookies will not work over HTTP. 5. **Trusted Origins**: Make sure every possible frontend origin (including protocol and port) is listed in `trustedOrigins` at the top level of your config [reference](https://github.com/better-auth/better-auth/issues/3874). 6. **Proxy/CDN**: If you use Cloudflare, Vercel, or any proxy/CDN, ensure it is not stripping or rewriting `Set-Cookie` headers. 7. **Browser Testing**: Some browsers (Safari, Brave, Firefox) block third-party cookies by default. Test in Chrome incognito to rule out browser-side issues. 8. **Session Validation**: If the session is created without a valid user id (e.g., due to a misconfigured OAuth provider or custom user mapping), Better Auth will erase the cookie immediately after login [reference](https://github.com/better-auth/better-auth/issues/4483). If you apply these changes and still see the issue, inspect the actual `Set-Cookie` headers in your network tab after sign-in and after redirect. The cookie should have the correct domain, path `/`, `SameSite=None`, and `Secure` attributes. If it’s missing or being deleted, it’s almost always a config or environment mismatch. Let me know if you want a checklist or want to debug specific headers! <!-- 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/9ad9f130-43e0-45ee-9734-457b0b9ec087?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9ad9f130-43e0-45ee-9734-457b0b9ec087?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9ad9f130-43e0-45ee-9734-457b0b9ec087?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9ad9f130-43e0-45ee-9734-457b0b9ec087?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9ad9f130-43e0-45ee-9734-457b0b9ec087?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9ad9f130-43e0-45ee-9734-457b0b9ec087?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/9ad9f130-43e0-45ee-9734-457b0b9ec087?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/5611)
Author
Owner

@sam-Encodev commented on GitHub (Nov 15, 2025):

There probably should be a blogpost on this.

<!-- gh-comment-id:3536650184 --> @sam-Encodev commented on GitHub (Nov 15, 2025): There probably should be a blogpost on this.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10300