CORS violation even after defining trustedOrigins #1727

Open
opened 2026-03-13 08:58:53 -05:00 by GiteaMirror · 19 comments
Owner

Originally created by @tech-savvy-guy on GitHub (Aug 17, 2025).

Originally assigned to: @himself65 on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

My better-auth server is deployed at "https://auth.citecat.com"

I want my application at "https://demo.auth.citecat.com" to use this auth server

This is my auth-client.ts config:

import { nextCookies } from "better-auth/next-js";
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
    baseURL: "https://auth.citecat.com",
    plugins: [nextCookies()],
});

I don't have an auth.ts for this application since it's using the auth server

Current vs. Expected behavior

I am getting a CORS violation error when I try to use the authClient in my application

Image

What version of Better Auth are you using?

1.3.6

System info

System:
  OS: Linux 6.14 Ubuntu 24.04.2 LTS 24.04.2 LTS (Noble Numbat)
  CPU: (12) x64 AMD Ryzen 5 4600H with Radeon Graphics
  Memory: 16.14 GB / 22.85 GB
  Container: Yes
  Shell: 5.9 - /usr/bin/zsh
Browsers:
  Brave Browser: 138.1.80.115
  Chrome: 138.0.7204.100

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

Client, Backend

Auth config (if applicable)

import { db } from "@/db/drizzle";
import { schema } from "@/db/schema";

import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

import { sendEmail } from "@/app/(auth)/utils/email"
import ForgotPassword from "@/app/(auth)/components/forgot-password";
import VerifyEmailTemplate from "@/app/(auth)/components/verify-email";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema,
  }),
  account: {
    accountLinking: {
      enabled: true,
    }
  },
  emailAndPassword: {
    enabled: true,
    sendResetPassword: async ({ user, url, token }, request) => {
      await sendEmail({
        to: user.email,
        subject: "Reset your password",
        template: ForgotPassword,
        templateProps: {
          resetUrl: url,
        }
      })
    }
  },
  emailVerification: {
    sendOnSignUp: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      await sendEmail({
        to: user.email,
        subject: "Verify your email address",
        template: VerifyEmailTemplate,
        templateProps: {
          verifyUrl: url,
        }
      })
    },
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID as string,
      clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
    },
    microsoft: {
      clientId: process.env.MICROSOFT_CLIENT_ID as string,
      clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string,
    },
  },
  trustedOrigins: [
    "https://citecat.com",
    "https://auth.citecat.com",
    "https://editor.citecat.com",
    "https://demo.auth.citecat.com",
    "http://localhost:3000", // Development environment
  ],
  advanced: {
    crossSubDomainCookies: {
      enabled: process.env.NODE_ENV === "production",
      domain: ".citecat.com",
    },
    useSecureCookies: true
  },
  plugins: [nextCookies()]
});

Additional context

No response

Originally created by @tech-savvy-guy on GitHub (Aug 17, 2025). Originally assigned to: @himself65 on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce My better-auth server is deployed at "https://auth.citecat.com" I want my application at "https://demo.auth.citecat.com" to use this auth server This is my `auth-client.ts` config: ```ts import { nextCookies } from "better-auth/next-js"; import { createAuthClient } from "better-auth/react"; export const authClient = createAuthClient({ baseURL: "https://auth.citecat.com", plugins: [nextCookies()], }); ``` I don't have an `auth.ts` for this application since it's using the auth server ### Current vs. Expected behavior I am getting a CORS violation error when I try to use the authClient in my application <img width="786" height="570" alt="Image" src="https://github.com/user-attachments/assets/138c36cd-8ee1-4483-b08d-03a09eda57c3" /> ### What version of Better Auth are you using? 1.3.6 ### System info ```bash System: OS: Linux 6.14 Ubuntu 24.04.2 LTS 24.04.2 LTS (Noble Numbat) CPU: (12) x64 AMD Ryzen 5 4600H with Radeon Graphics Memory: 16.14 GB / 22.85 GB Container: Yes Shell: 5.9 - /usr/bin/zsh Browsers: Brave Browser: 138.1.80.115 Chrome: 138.0.7204.100 ``` ### Which area(s) are affected? (Select all that apply) Client, Backend ### Auth config (if applicable) ```typescript import { db } from "@/db/drizzle"; import { schema } from "@/db/schema"; import { betterAuth } from "better-auth"; import { nextCookies } from "better-auth/next-js"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { sendEmail } from "@/app/(auth)/utils/email" import ForgotPassword from "@/app/(auth)/components/forgot-password"; import VerifyEmailTemplate from "@/app/(auth)/components/verify-email"; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema, }), account: { accountLinking: { enabled: true, } }, emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url, token }, request) => { await sendEmail({ to: user.email, subject: "Reset your password", template: ForgotPassword, templateProps: { resetUrl: url, } }) } }, emailVerification: { sendOnSignUp: true, sendVerificationEmail: async ({ user, url, token }, request) => { await sendEmail({ to: user.email, subject: "Verify your email address", template: VerifyEmailTemplate, templateProps: { verifyUrl: url, } }) }, }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, github: { clientId: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, }, microsoft: { clientId: process.env.MICROSOFT_CLIENT_ID as string, clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string, }, }, trustedOrigins: [ "https://citecat.com", "https://auth.citecat.com", "https://editor.citecat.com", "https://demo.auth.citecat.com", "http://localhost:3000", // Development environment ], advanced: { crossSubDomainCookies: { enabled: process.env.NODE_ENV === "production", domain: ".citecat.com", }, useSecureCookies: true }, plugins: [nextCookies()] }); ``` ### Additional context _No response_
GiteaMirror added the nextjsbug labels 2026-03-13 08:58:53 -05:00
Author
Owner

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

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

The most common cause of CORS errors with better-auth—even when trustedOrigins is set—is an exact mismatch between the browser's Origin header and the entries in trustedOrigins. The match is case-sensitive and must include the protocol, hostname, and port. Double-check the Origin value in your browser's network tab for the failing request and make sure it matches one of your trustedOrigins exactly (no trailing slashes, correct scheme, etc) reference.

trustedOrigins must be defined at the top level of your config, not inside advanced. After making changes, restart your backend to apply them reference.

If you use any other CORS middleware (like Fastify, Express, or a reverse proxy), make sure only one CORS handler is active for your auth endpoints. Conflicts between better-auth's internal CORS and external middleware are a frequent source of missing headers reference.

Also, ensure your frontend requests use credentials: 'include', and cookies are set with sameSite: 'none' and secure: true for cross-origin cookie sharing reference.

If you still see CORS errors, check for any reverse proxy or gateway (like nginx or API Gateway) that might be stripping or rewriting Origin headers, and confirm its CORS config allows your client origin reference.

If the problem persists, please share the full request and response headers from a failed request and check your server logs for CORS or FORBIDDEN errors.

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 (Aug 17, 2025): <!-- Greeting --> Hi @tech-savvy-guy! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The most common cause of CORS errors with better-auth—even when trustedOrigins is set—is an exact mismatch between the browser's Origin header and the entries in trustedOrigins. The match is case-sensitive and must include the protocol, hostname, and port. Double-check the Origin value in your browser's network tab for the failing request and make sure it matches one of your trustedOrigins exactly (no trailing slashes, correct scheme, etc) [reference](https://github.com/better-auth/better-auth/issues/3874#issuecomment-3166880225). trustedOrigins must be defined at the top level of your config, not inside advanced. After making changes, restart your backend to apply them [reference](https://github.com/better-auth/better-auth/issues/3874#issuecomment-3166880225). If you use any other CORS middleware (like Fastify, Express, or a reverse proxy), make sure only one CORS handler is active for your auth endpoints. Conflicts between better-auth's internal CORS and external middleware are a frequent source of missing headers [reference](https://github.com/better-auth/better-auth/issues/4021#issuecomment-3190597020). Also, ensure your frontend requests use credentials: 'include', and cookies are set with sameSite: 'none' and secure: true for cross-origin cookie sharing [reference](https://github.com/better-auth/better-auth/issues/3874#issuecomment-3166880225). If you still see CORS errors, check for any reverse proxy or gateway (like nginx or API Gateway) that might be stripping or rewriting Origin headers, and confirm its CORS config allows your client origin [reference](https://github.com/better-auth/better-auth/issues/4021#issuecomment-3190597020). If the problem persists, please share the full request and response headers from a failed request and check your server logs for CORS or FORBIDDEN errors. <!-- 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/cfe3070a-fd44-41b5-b592-086e3efbbfe2?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/cfe3070a-fd44-41b5-b592-086e3efbbfe2?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/cfe3070a-fd44-41b5-b592-086e3efbbfe2?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/cfe3070a-fd44-41b5-b592-086e3efbbfe2?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/cfe3070a-fd44-41b5-b592-086e3efbbfe2?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/cfe3070a-fd44-41b5-b592-086e3efbbfe2?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/cfe3070a-fd44-41b5-b592-086e3efbbfe2?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/4052)
Author
Owner

@tech-savvy-guy commented on GitHub (Aug 17, 2025):

If the problem persists, please share the full request and response headers from a failed request and check your server logs for CORS or FORBIDDEN errors.

To reply, just mention @dosu.

Here's the full request:

curl 'https://auth.citecat.com/api/auth/get-session' \
  -H 'accept: */*' \
  -H 'accept-language: en-US,en;q=0.9' \
  -b 'authentik_csrf=pMCPk82AOHJMTa2dUvYEXP24o1Z6nnV7; ph_phc_8GkksCmx4fOEbU7fhjUUONa2o7j9M2PiwGR4ZOk7m7O_posthog=%7B%22distinct_id%22%3A%2201984260-5438-7200-bcd9-800d979d2a45%22%2C%22%24sesid%22%3A%5B1755277622430%2C%220198aeb2-584f-739d-89e9-e292581a0781%22%2C1755277580367%5D%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fcitecat.com%2Flogin%22%7D%7D; __Secure-better-auth.session_token=fyXMPyVowBGqLixpHVm6a78vPoxw6aQJ.JkJEwGePp9LNNLJsi5qUP5YerNhYUZoYJdQ7xE7AuDo%3D' \
  -H 'origin: https://demo.auth.citecat.com' \
  -H 'priority: u=1, i' \
  -H 'referer: https://demo.auth.citecat.com/' \
  -H 'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Linux"' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: same-site' \
  -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
@tech-savvy-guy commented on GitHub (Aug 17, 2025): > If the problem persists, please share the full request and response headers from a failed request and check your server logs for CORS or FORBIDDEN errors. > > _To reply, just mention [@dosu](https://go.dosu.dev/dosubot)._ Here's the full request: ``` curl 'https://auth.citecat.com/api/auth/get-session' \ -H 'accept: */*' \ -H 'accept-language: en-US,en;q=0.9' \ -b 'authentik_csrf=pMCPk82AOHJMTa2dUvYEXP24o1Z6nnV7; ph_phc_8GkksCmx4fOEbU7fhjUUONa2o7j9M2PiwGR4ZOk7m7O_posthog=%7B%22distinct_id%22%3A%2201984260-5438-7200-bcd9-800d979d2a45%22%2C%22%24sesid%22%3A%5B1755277622430%2C%220198aeb2-584f-739d-89e9-e292581a0781%22%2C1755277580367%5D%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fcitecat.com%2Flogin%22%7D%7D; __Secure-better-auth.session_token=fyXMPyVowBGqLixpHVm6a78vPoxw6aQJ.JkJEwGePp9LNNLJsi5qUP5YerNhYUZoYJdQ7xE7AuDo%3D' \ -H 'origin: https://demo.auth.citecat.com' \ -H 'priority: u=1, i' \ -H 'referer: https://demo.auth.citecat.com/' \ -H 'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' \ -H 'sec-ch-ua-mobile: ?0' \ -H 'sec-ch-ua-platform: "Linux"' \ -H 'sec-fetch-dest: empty' \ -H 'sec-fetch-mode: cors' \ -H 'sec-fetch-site: same-site' \ -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' ```
Author
Owner

@lord007tn commented on GitHub (Aug 18, 2025):

same here,
i tried everything, only better-auth requests are not handled correctly,

i think this is a problem with 1.3.7

i will try other versions and see if it works

@lord007tn commented on GitHub (Aug 18, 2025): same here, i tried everything, only better-auth requests are not handled correctly, i think this is a problem with 1.3.7 i will try other versions and see if it works
Author
Owner

@melsonic commented on GitHub (Aug 18, 2025):

From the initial look, it looks like the issue is with originCheckMiddleware which only allows POST requests as of now.
9a6a8d741b/packages/better-auth/src/api/middlewares/origin-check.ts (L11-L14)
I'll test it once & in case it solves the issue, will raise a PR 👍

@melsonic commented on GitHub (Aug 18, 2025): From the initial look, it looks like the issue is with `originCheckMiddleware` which only allows POST requests as of now. https://github.com/better-auth/better-auth/blob/9a6a8d741bf77c0d2e44c02281314b50a75cd6fc/packages/better-auth/src/api/middlewares/origin-check.ts#L11-L14 I'll test it once & in case it solves the issue, will raise a PR 👍
Author
Owner

@tech-savvy-guy commented on GitHub (Aug 18, 2025):

i will try other versions and see if it works

did you manage to get this working?

@tech-savvy-guy commented on GitHub (Aug 18, 2025): > i will try other versions and see if it works did you manage to get this working?
Author
Owner

@lord007tn commented on GitHub (Aug 18, 2025):

@tech-savvy-guy still facing the same issue, and its not related to hono server
because the same server with other requests from the browser works, only the better-auth related requests have a CORS error

also the same better-auth implementation works in localhost when the server is in localhost, the issue rises when i host the server to the cloud.

also when i see the originCheckMiddleware maybe its conflicting with the current setup of hono

i even tried returning the origin as it is

const corsOptions: Parameters<typeof cors>[0] = {
  origin: (origin) => origin,
  allowHeaders: ['Content-Type', 'Authorization'],
  allowMethods: ['POST', 'GET', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'],
  exposeHeaders: ['Content-Length'],
  maxAge: 600,
  credentials: true,
};

This server implementation works with my Vue app, and i can authenticate with no problem, the same one doesn't work in NextJS
i even make sure that the server implementation is similar in both apps, hosted on the same server, same Docker compose, what changes is the frontend that consumes it

@lord007tn commented on GitHub (Aug 18, 2025): @tech-savvy-guy still facing the same issue, and its not related to hono server because the same server with other requests from the browser works, only the better-auth related requests have a CORS error also the same better-auth implementation works in localhost when the server is in localhost, the issue rises when i host the server to the cloud. also when i see the `originCheckMiddleware` maybe its conflicting with the current setup of hono i even tried returning the origin as it is ```ts const corsOptions: Parameters<typeof cors>[0] = { origin: (origin) => origin, allowHeaders: ['Content-Type', 'Authorization'], allowMethods: ['POST', 'GET', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'], exposeHeaders: ['Content-Length'], maxAge: 600, credentials: true, }; ``` This server implementation works with my Vue app, and i can authenticate with no problem, the same one doesn't work in NextJS i even make sure that the server implementation is similar in both apps, hosted on the same server, same Docker compose, what changes is the frontend that consumes it
Author
Owner

@tech-savvy-guy commented on GitHub (Aug 18, 2025):

Well, I did manage to get this working by explicitly allowing CORS in my /api/auth route -

import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

const { GET: baseGet, POST: basePost } = toNextJsHandler(auth);

const allowedOrigins = new Set([
  "https://example.com",
  "https://auth.example.com",
  "http://localhost:3000", // Development environment
]);

const corsHeaders = {
  "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
  "Access-Control-Allow-Credentials": "true",
};

function isOriginAllowed(origin: string): boolean {
  return allowedOrigins.has(origin);
}

function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) {
  return new Response(body, {
    status,
    headers: {
      ...corsHeaders,
      "Access-Control-Allow-Origin": origin,
    },
  });
}

function withCors(handler: (req: Request) => Promise<Response>) {
  return async (req: Request): Promise<Response> => {
    const origin = req.headers.get("origin") ?? "";

    if (!isOriginAllowed(origin)) {
      return new Response("CORS not allowed", { status: 403 });
    }

    if (req.method === "OPTIONS") {
      return buildCorsResponse(origin, 204);
    }

    const res = await handler(req);

    const response = new Response(res.body, res);
    for (const [key, value] of Object.entries(corsHeaders)) {
      response.headers.set(key, value);
    }
    response.headers.set("Access-Control-Allow-Origin", origin);

    return response;
  };
}

export const GET = withCors(baseGet);
export const POST = withCors(basePost);

@tech-savvy-guy commented on GitHub (Aug 18, 2025): Well, I did manage to get this working by explicitly allowing `CORS` in my `/api/auth` route - ```ts import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; const { GET: baseGet, POST: basePost } = toNextJsHandler(auth); const allowedOrigins = new Set([ "https://example.com", "https://auth.example.com", "http://localhost:3000", // Development environment ]); const corsHeaders = { "Access-Control-Allow-Methods": "GET,POST,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Allow-Credentials": "true", }; function isOriginAllowed(origin: string): boolean { return allowedOrigins.has(origin); } function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) { return new Response(body, { status, headers: { ...corsHeaders, "Access-Control-Allow-Origin": origin, }, }); } function withCors(handler: (req: Request) => Promise<Response>) { return async (req: Request): Promise<Response> => { const origin = req.headers.get("origin") ?? ""; if (!isOriginAllowed(origin)) { return new Response("CORS not allowed", { status: 403 }); } if (req.method === "OPTIONS") { return buildCorsResponse(origin, 204); } const res = await handler(req); const response = new Response(res.body, res); for (const [key, value] of Object.entries(corsHeaders)) { response.headers.set(key, value); } response.headers.set("Access-Control-Allow-Origin", origin); return response; }; } export const GET = withCors(baseGet); export const POST = withCors(basePost); ```
Author
Owner

@victorbuikem commented on GitHub (Aug 18, 2025):

for me i am tunneling my local server through ngrok and i ran into this issues. i have tried

  • adding trustedOrigins option in better-auth config
  • writing an OPTIONS handler for the auth route
  • changing the BASE_URL env Variable
  • finally @tech-savvy-guy CORS implementation
    and these are not working. the OPTION request still returns 204
@victorbuikem commented on GitHub (Aug 18, 2025): for me i am tunneling my local server through ngrok and i ran into this issues. i have tried - adding trustedOrigins option in better-auth config - writing an OPTIONS handler for the auth route - changing the BASE_URL env Variable - finally @tech-savvy-guy CORS implementation and these are not working. the OPTION request still returns 204
Author
Owner

@tech-savvy-guy commented on GitHub (Aug 26, 2025):

update: even auth instance doesn't work properly when I use a different baseURL @himself65 @Bekacru

it seems I can't access the API in server components when I use a different baseURL for the auth instance #4188

@tech-savvy-guy commented on GitHub (Aug 26, 2025): update: even `auth` instance doesn't work properly when I use a different `baseURL` @himself65 @Bekacru it seems I can't access the API in server components when I use a different baseURL for the `auth` instance #4188
Author
Owner

@himself65 commented on GitHub (Aug 26, 2025):

update: even auth instance doesn't work properly when I use a different baseURL @himself65 @Bekacru

it seems I can't access the API in server components when I use a different baseURL for the auth instance #4188

What’s your configuration?

@himself65 commented on GitHub (Aug 26, 2025): > update: even `auth` instance doesn't work properly when I use a different `baseURL` [@himself65](https://github.com/himself65) [@Bekacru](https://github.com/Bekacru) > > it seems I can't access the API in server components when I use a different baseURL for the `auth` instance [#4188](https://github.com/better-auth/better-auth/issues/4188) What’s your configuration?
Author
Owner

@tech-savvy-guy commented on GitHub (Aug 26, 2025):

Auth config (if applicable)

import { db } from "@/db/drizzle";
import { schema } from "@/db/schema";

import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
import { drizzleAdapter } from "better-auth/adapters/drizzle";

import { sendEmail } from "@/app/(auth)/utils/email"
import ForgotPassword from "@/app/(auth)/components/forgot-password";
import VerifyEmailTemplate from "@/app/(auth)/components/verify-email";

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
    schema,
  }),
  account: {
    accountLinking: {
      enabled: true,
    }
  },
  emailAndPassword: {
    enabled: true,
    sendResetPassword: async ({ user, url, token }, request) => {
      await sendEmail({
        to: user.email,
        subject: "Reset your password",
        template: ForgotPassword,
        templateProps: {
          resetUrl: url,
        }
      })
    }
  },
  emailVerification: {
    sendOnSignUp: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      await sendEmail({
        to: user.email,
        subject: "Verify your email address",
        template: VerifyEmailTemplate,
        templateProps: {
          verifyUrl: url,
        }
      })
    },
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID as string,
      clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
    },
    microsoft: {
      clientId: process.env.MICROSOFT_CLIENT_ID as string,
      clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string,
    },
  },
  trustedOrigins: [
    "https://citecat.com",
    "https://auth.citecat.com",
    "https://editor.citecat.com",
    "https://demo.auth.citecat.com",
    "http://localhost:3000", // Development environment
  ],
  advanced: {
    crossSubDomainCookies: {
      enabled: process.env.NODE_ENV === "production",
      domain: ".citecat.com",
    },
    useSecureCookies: true
  },
  plugins: [nextCookies()]
});

Here's my config 👆🏻

@tech-savvy-guy commented on GitHub (Aug 26, 2025): > ### Auth config (if applicable) > > ```typescript > import { db } from "@/db/drizzle"; > import { schema } from "@/db/schema"; > > import { betterAuth } from "better-auth"; > import { nextCookies } from "better-auth/next-js"; > import { drizzleAdapter } from "better-auth/adapters/drizzle"; > > import { sendEmail } from "@/app/(auth)/utils/email" > import ForgotPassword from "@/app/(auth)/components/forgot-password"; > import VerifyEmailTemplate from "@/app/(auth)/components/verify-email"; > > export const auth = betterAuth({ > database: drizzleAdapter(db, { > provider: "pg", > schema, > }), > account: { > accountLinking: { > enabled: true, > } > }, > emailAndPassword: { > enabled: true, > sendResetPassword: async ({ user, url, token }, request) => { > await sendEmail({ > to: user.email, > subject: "Reset your password", > template: ForgotPassword, > templateProps: { > resetUrl: url, > } > }) > } > }, > emailVerification: { > sendOnSignUp: true, > sendVerificationEmail: async ({ user, url, token }, request) => { > await sendEmail({ > to: user.email, > subject: "Verify your email address", > template: VerifyEmailTemplate, > templateProps: { > verifyUrl: url, > } > }) > }, > }, > socialProviders: { > google: { > clientId: process.env.GOOGLE_CLIENT_ID as string, > clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, > }, > github: { > clientId: process.env.GITHUB_CLIENT_ID as string, > clientSecret: process.env.GITHUB_CLIENT_SECRET as string, > }, > microsoft: { > clientId: process.env.MICROSOFT_CLIENT_ID as string, > clientSecret: process.env.MICROSOFT_CLIENT_SECRET as string, > }, > }, > trustedOrigins: [ > "https://citecat.com", > "https://auth.citecat.com", > "https://editor.citecat.com", > "https://demo.auth.citecat.com", > "http://localhost:3000", // Development environment > ], > advanced: { > crossSubDomainCookies: { > enabled: process.env.NODE_ENV === "production", > domain: ".citecat.com", > }, > useSecureCookies: true > }, > plugins: [nextCookies()] > }); > ``` Here's my config 👆🏻
Author
Owner

@himself65 commented on GitHub (Aug 26, 2025):

Is it because your crossSubDomainCookies.domain? Is that the same as your current frontend URL?

@himself65 commented on GitHub (Aug 26, 2025): Is it because your crossSubDomainCookies.domain? Is that the same as your current frontend URL?
Author
Owner

@Ot7struggle commented on GitHub (Aug 30, 2025):

Well, I did manage to get this working by explicitly allowing CORS in my /api/auth route -

import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

const { GET: baseGet, POST: basePost } = toNextJsHandler(auth);

const allowedOrigins = new Set([
"https://example.com",
"https://auth.example.com",
"http://localhost:3000", // Development environment
]);

const corsHeaders = {
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
};

function isOriginAllowed(origin: string): boolean {
return allowedOrigins.has(origin);
}

function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) {
return new Response(body, {
status,
headers: {
...corsHeaders,
"Access-Control-Allow-Origin": origin,
},
});
}

function withCors(handler: (req: Request) => Promise) {
return async (req: Request): Promise => {
const origin = req.headers.get("origin") ?? "";

if (!isOriginAllowed(origin)) {
  return new Response("CORS not allowed", { status: 403 });
}

if (req.method === "OPTIONS") {
  return buildCorsResponse(origin, 204);
}

const res = await handler(req);

const response = new Response(res.body, res);
for (const [key, value] of Object.entries(corsHeaders)) {
  response.headers.set(key, value);
}
response.headers.set("Access-Control-Allow-Origin", origin);

return response;

};
}

export const GET = withCors(baseGet);
export const POST = withCors(basePost);

Hi I used this, but I am still getting an error: Access to fetch at 'http://localhost:3000/api/auth/sign-in/social' from origin 'https://bstream.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
localhost:3000/api/auth/sign-in/social:1 Failed to load resource: net::ERR_FAILED I am trying to click on Google Sign-in, but that error is popping up, and I have used everything suggested

@Ot7struggle commented on GitHub (Aug 30, 2025): > Well, I did manage to get this working by explicitly allowing `CORS` in my `/api/auth` route - > > import { auth } from "@/lib/auth"; > import { toNextJsHandler } from "better-auth/next-js"; > > const { GET: baseGet, POST: basePost } = toNextJsHandler(auth); > > const allowedOrigins = new Set([ > "https://example.com", > "https://auth.example.com", > "http://localhost:3000", // Development environment > ]); > > const corsHeaders = { > "Access-Control-Allow-Methods": "GET,POST,OPTIONS", > "Access-Control-Allow-Headers": "Content-Type, Authorization", > "Access-Control-Allow-Credentials": "true", > }; > > function isOriginAllowed(origin: string): boolean { > return allowedOrigins.has(origin); > } > > function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) { > return new Response(body, { > status, > headers: { > ...corsHeaders, > "Access-Control-Allow-Origin": origin, > }, > }); > } > > function withCors(handler: (req: Request) => Promise<Response>) { > return async (req: Request): Promise<Response> => { > const origin = req.headers.get("origin") ?? ""; > > if (!isOriginAllowed(origin)) { > return new Response("CORS not allowed", { status: 403 }); > } > > if (req.method === "OPTIONS") { > return buildCorsResponse(origin, 204); > } > > const res = await handler(req); > > const response = new Response(res.body, res); > for (const [key, value] of Object.entries(corsHeaders)) { > response.headers.set(key, value); > } > response.headers.set("Access-Control-Allow-Origin", origin); > > return response; > }; > } > > export const GET = withCors(baseGet); > export const POST = withCors(basePost); Hi I used this, but I am still getting an error: Access to fetch at 'http://localhost:3000/api/auth/sign-in/social' from origin 'https://bstream.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. localhost:3000/api/auth/sign-in/social:1 Failed to load resource: net::ERR_FAILED I am trying to click on Google Sign-in, but that error is popping up, and I have used everything suggested
Author
Owner

@Cybernetixs commented on GitHub (Sep 8, 2025):

I am also having CORS issues from a static export NextJS/better-auth site on S3.

All other requests to the api server with better-auth work fine, with no CORS error, its only the better-auth client requests that throw the CORS errors. If I use postman to hit the /api/auth/sign-in/email endpoint I do not get a CORS error, so it is definitely something with the auth client.

Until this is resolved, is there a way to make the direct call to the better-auth api endpoint and set the session ?
I tried doing a fetch with the same body that is sent by the client but it gives a 400 response with : {"code":"VALIDATION_ERROR","message":"Invalid body parameters"}

@Cybernetixs commented on GitHub (Sep 8, 2025): I am also having CORS issues from a static export NextJS/better-auth site on S3. All other requests to the api server with better-auth work fine, with no CORS error, its only the better-auth client requests that throw the CORS errors. If I use postman to hit the /api/auth/sign-in/email endpoint I do not get a CORS error, so it is definitely something with the auth client. Until this is resolved, is there a way to make the direct call to the better-auth api endpoint and set the session ? I tried doing a fetch with the same body that is sent by the client but it gives a 400 response with : {"code":"VALIDATION_ERROR","message":"Invalid body parameters"}
Author
Owner

@Cybernetixs commented on GitHub (Sep 8, 2025):

I am also having CORS issues from a static export NextJS/better-auth site on S3.

All other requests to the api server with better-auth work fine, with no CORS error, its only the better-auth client requests that throw the CORS errors. If I use postman to hit the /api/auth/sign-in/email endpoint I do not get a CORS error, so it is definitely something with the auth client.

Until this is resolved, is there a way to make the direct call to the better-auth api endpoint and set the session ? I tried doing a fetch with the same body that is sent by the client but it gives a 400 response with : {"code":"VALIDATION_ERROR","message":"Invalid body parameters"}


Not sure if this is related to the other CORS issues here since mine was specific to a static export site,
but I was able to get this working and ran across something weird.

I have my signup page in its own component, and it was being imported and rendered by the page.tsx at the /sign-in route.
That is where I kept getting the CORS issues.

Then just trial and error, I imported and rendered the signup component from the page.tsx at the base route /
And voila ! The CORS error went away.

I verified this by keeping the signup page loaded at both routes, root route and /sign-up
The component loaded from the /sign-up route consistently gives CORS errors, whereas the one at / works just fine.


Update: Sorry, that up above actually didn't fix the "client auth" issue, I didn't invalidate the cloudfront distribution so I was getting an old cache.

I was only able to make this work by making direct fetch calls to the better-auth api server without using the auth client on the front end.

@Cybernetixs commented on GitHub (Sep 8, 2025): > I am also having CORS issues from a static export NextJS/better-auth site on S3. > > All other requests to the api server with better-auth work fine, with no CORS error, its only the better-auth client requests that throw the CORS errors. If I use postman to hit the /api/auth/sign-in/email endpoint I do not get a CORS error, so it is definitely something with the auth client. > > Until this is resolved, is there a way to make the direct call to the better-auth api endpoint and set the session ? I tried doing a fetch with the same body that is sent by the client but it gives a 400 response with : {"code":"VALIDATION_ERROR","message":"Invalid body parameters"} ------------------------------------------------------------------------------------------------------- Not sure if this is related to the other CORS issues here since mine was specific to a static export site, but I was able to get this working and ran across something weird. I have my signup page in its own component, and it was being imported and rendered by the page.tsx at the /sign-in route. That is where I kept getting the CORS issues. Then just trial and error, I imported and rendered the signup component from the page.tsx at the base route / And voila ! The CORS error went away. I verified this by keeping the signup page loaded at both routes, root route and /sign-up The component loaded from the /sign-up route consistently gives CORS errors, whereas the one at / works just fine. ----------------------------------------------------------------------------------------------------------- Update: Sorry, that up above actually didn't fix the "client auth" issue, I didn't invalidate the cloudfront distribution so I was getting an old cache. I was only able to make this work by making direct fetch calls to the better-auth api server without using the auth client on the front end.
Author
Owner

@oxedom commented on GitHub (Nov 6, 2025):

Just wanted to follow up and share the solution in case it helps others facing a similar issue.

The root cause turned out to be on the frontend side. I had my button wrapped in a form element, and each click was triggering a form submission that redirected to domain/?. This was causing the CORS requests to fail, which initially made me think it was a server configuration problem.

Solution: Added e.preventDefault() to the button's click handler to prevent the default form submission behavior.
It was one of those subtle issues that's easy to overlook but can cause quite a bit of confusion. Hopefully this saves someone else some debugging time!

@oxedom commented on GitHub (Nov 6, 2025): Just wanted to follow up and share the solution in case it helps others facing a similar issue. The root cause turned out to be on the frontend side. I had my button wrapped in a form element, and each click was triggering a form submission that redirected to domain/?. This was causing the CORS requests to fail, which initially made me think it was a server configuration problem. Solution: Added e.preventDefault() to the button's click handler to prevent the default form submission behavior. It was one of those subtle issues that's easy to overlook but can cause quite a bit of confusion. Hopefully this saves someone else some debugging time!
Author
Owner

@laxya-gudz commented on GitHub (Nov 29, 2025):

If anyone facing the same issue with Tanstack Start you can use this in your catch all route file

`import { createFileRoute } from "@tanstack/react-router";
import { auth } from "@/lib/auth";

const allowedOrigins = new Set([
"https://example.com",
"https://auth.example.com",
"http://localhost:3000", // Development environment
"http://localhost:3001", // Customer app
]);

const corsHeaders = {
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
};

function isOriginAllowed(origin: string): boolean {
return allowedOrigins.has(origin);
}

function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) {
return new Response(body, {
status,
headers: {
...corsHeaders,
"Access-Control-Allow-Origin": origin,
},
});
}

function withCors(handler: (req: Request) => Promise) {
return async (req: Request): Promise => {
const origin = req.headers.get("origin") ?? "";

	if (!isOriginAllowed(origin)) {
		return new Response("CORS not allowed", { status: 403 });
	}

	if (req.method === "OPTIONS") {
		return buildCorsResponse(origin, 204);
	}

	const res = await handler(req);

	const response = new Response(res.body, res);

	for (const [key, value] of Object.entries(corsHeaders)) {
		response.headers.set(key, value);
	}

	response.headers.set("Access-Control-Allow-Origin", origin);

	return response;
};

}

const baseHandler = withCors(auth.handler);

export const Route = createFileRoute("/api/auth/$")({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => {
return await baseHandler(request);
},
POST: async ({ request }: { request: Request }) => {
return await baseHandler(request);
},
OPTIONS: async ({ request }: { request: Request }) => {
const origin = request.headers.get("origin") ?? "";
if (!isOriginAllowed(origin)) {
return new Response("CORS not allowed", { status: 403 });
}
return buildCorsResponse(origin, 204);
},
},
},
});
`

@laxya-gudz commented on GitHub (Nov 29, 2025): If anyone facing the same issue with Tanstack Start you can use this in your catch all route file `import { createFileRoute } from "@tanstack/react-router"; import { auth } from "@/lib/auth"; const allowedOrigins = new Set([ "https://example.com", "https://auth.example.com", "http://localhost:3000", // Development environment "http://localhost:3001", // Customer app ]); const corsHeaders = { "Access-Control-Allow-Methods": "GET,POST,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Allow-Credentials": "true", }; function isOriginAllowed(origin: string): boolean { return allowedOrigins.has(origin); } function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) { return new Response(body, { status, headers: { ...corsHeaders, "Access-Control-Allow-Origin": origin, }, }); } function withCors(handler: (req: Request) => Promise<Response>) { return async (req: Request): Promise<Response> => { const origin = req.headers.get("origin") ?? ""; if (!isOriginAllowed(origin)) { return new Response("CORS not allowed", { status: 403 }); } if (req.method === "OPTIONS") { return buildCorsResponse(origin, 204); } const res = await handler(req); const response = new Response(res.body, res); for (const [key, value] of Object.entries(corsHeaders)) { response.headers.set(key, value); } response.headers.set("Access-Control-Allow-Origin", origin); return response; }; } const baseHandler = withCors(auth.handler); export const Route = createFileRoute("/api/auth/$")({ server: { handlers: { GET: async ({ request }: { request: Request }) => { return await baseHandler(request); }, POST: async ({ request }: { request: Request }) => { return await baseHandler(request); }, OPTIONS: async ({ request }: { request: Request }) => { const origin = request.headers.get("origin") ?? ""; if (!isOriginAllowed(origin)) { return new Response("CORS not allowed", { status: 403 }); } return buildCorsResponse(origin, 204); }, }, }, }); `
Author
Owner

@zotodev commented on GitHub (Jan 15, 2026):

If anyone facing the same issue with Tanstack Start you can use this in your catch all route file

`import { createFileRoute } from "@tanstack/react-router"; import { auth } from "@/lib/auth";

const allowedOrigins = new Set([ "https://example.com", "https://auth.example.com", "http://localhost:3000", // Development environment "http://localhost:3001", // Customer app ]);

const corsHeaders = { "Access-Control-Allow-Methods": "GET,POST,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Allow-Credentials": "true", };

function isOriginAllowed(origin: string): boolean { return allowedOrigins.has(origin); }

function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) { return new Response(body, { status, headers: { ...corsHeaders, "Access-Control-Allow-Origin": origin, }, }); }

function withCors(handler: (req: Request) => Promise) { return async (req: Request): Promise => { const origin = req.headers.get("origin") ?? "";

	if (!isOriginAllowed(origin)) {
		return new Response("CORS not allowed", { status: 403 });
	}

	if (req.method === "OPTIONS") {
		return buildCorsResponse(origin, 204);
	}

	const res = await handler(req);

	const response = new Response(res.body, res);

	for (const [key, value] of Object.entries(corsHeaders)) {
		response.headers.set(key, value);
	}

	response.headers.set("Access-Control-Allow-Origin", origin);

	return response;
};

}

const baseHandler = withCors(auth.handler);

export const Route = createFileRoute("/api/auth/$")({ server: { handlers: { GET: async ({ request }: { request: Request }) => { return await baseHandler(request); }, POST: async ({ request }: { request: Request }) => { return await baseHandler(request); }, OPTIONS: async ({ request }: { request: Request }) => { const origin = request.headers.get("origin") ?? ""; if (!isOriginAllowed(origin)) { return new Response("CORS not allowed", { status: 403 }); } return buildCorsResponse(origin, 204); }, }, }, }); `

I am still getting the same error after using this in catch all route

@zotodev commented on GitHub (Jan 15, 2026): > If anyone facing the same issue with Tanstack Start you can use this in your catch all route file > > `import { createFileRoute } from "@tanstack/react-router"; import { auth } from "@/lib/auth"; > > const allowedOrigins = new Set([ "https://example.com", "https://auth.example.com", "http://localhost:3000", // Development environment "http://localhost:3001", // Customer app ]); > > const corsHeaders = { "Access-Control-Allow-Methods": "GET,POST,OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Allow-Credentials": "true", }; > > function isOriginAllowed(origin: string): boolean { return allowedOrigins.has(origin); } > > function buildCorsResponse(origin: string, status: number, body: BodyInit | null = null) { return new Response(body, { status, headers: { ...corsHeaders, "Access-Control-Allow-Origin": origin, }, }); } > > function withCors(handler: (req: Request) => Promise) { return async (req: Request): Promise => { const origin = req.headers.get("origin") ?? ""; > > ``` > if (!isOriginAllowed(origin)) { > return new Response("CORS not allowed", { status: 403 }); > } > > if (req.method === "OPTIONS") { > return buildCorsResponse(origin, 204); > } > > const res = await handler(req); > > const response = new Response(res.body, res); > > for (const [key, value] of Object.entries(corsHeaders)) { > response.headers.set(key, value); > } > > response.headers.set("Access-Control-Allow-Origin", origin); > > return response; > }; > ``` > > } > > const baseHandler = withCors(auth.handler); > > export const Route = createFileRoute("/api/auth/$")({ server: { handlers: { GET: async ({ request }: { request: Request }) => { return await baseHandler(request); }, POST: async ({ request }: { request: Request }) => { return await baseHandler(request); }, OPTIONS: async ({ request }: { request: Request }) => { const origin = request.headers.get("origin") ?? ""; if (!isOriginAllowed(origin)) { return new Response("CORS not allowed", { status: 403 }); } return buildCorsResponse(origin, 204); }, }, }, }); ` I am still getting the same error after using this in catch all route
Author
Owner

@madaxen86 commented on GitHub (Jan 21, 2026):

I am still getting the same error after using this in catch all route

I could fix it by exporting an OPTIONS method explicitly in addition to this
https://github.com/better-auth/better-auth/issues/4052#issuecomment-3197763441


export const OPTIONS = (req: Request) => {
	const origin = req.headers.get("origin") ?? "";
	if (!isOriginAllowed(origin)) {
		return new Response("CORS not allowed", { status: 403 });
	}
	return buildCorsResponse(origin, 204);
};


@madaxen86 commented on GitHub (Jan 21, 2026): > I am still getting the same error after using this in catch all route I could fix it by exporting an OPTIONS method explicitly in addition to this https://github.com/better-auth/better-auth/issues/4052#issuecomment-3197763441 ```ts export const OPTIONS = (req: Request) => { const origin = req.headers.get("origin") ?? ""; if (!isOriginAllowed(origin)) { return new Response("CORS not allowed", { status: 403 }); } return buildCorsResponse(origin, 204); }; ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1727