[GH-ISSUE #5951] Google OAuth and separate backend #27707

Closed
opened 2026-04-17 18:52:48 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @felipinas on GitHub (Nov 13, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5951

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Ex:

  1. Create a server in Fastify
  2. Create a client in NextJS
  3. Create the Google OAuth config
  4. Try to sign in with this provider
  5. The cookie is not set on browser

Current vs. Expected behavior

Following the steps from above, I expected to store the cookies in the browser as well, but I just can only do it if I use an "internal" auth method, like OTP.

It seems like some redirection or setup error, I dunno.

What version of Better Auth are you using?

1.3.13 (Downgraded to this one cause I was getting a state_mismatch)

System info

"system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:29:54 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8122",
    "release": "24.6.0",
    "cpuCount": 8,
    "cpuModel": "Apple M3",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.09 GB"
  },
  "node": {
    "version": "v22.20.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.3"
  },
  "frameworks": [
    {
      "name": "fastify",
      "version": "^5.6.1"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "^6.19.0"
    }
  ],
}

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

Backend

Auth config (if applicable)

On fastify:

import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { emailOTP, openAPI, organization } from "better-auth/plugins";

import { env } from "@/env";
import { prisma } from "@/lib/db";

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  plugins: [
    emailOTP({
      async sendVerificationOTP({ email, otp, type }) {
        console.log("Sending OTP to", email, otp, type);
        if (type === "sign-in") {
          // Send the OTP for sign in
        } else if (type === "email-verification") {
          // Send the OTP for email verification
        } else {
          // Send the OTP for password reset
        }
      },
    }),
    openAPI(),
    organization(),
  ],
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
    },
  },
  trustedOrigins: env.BETTER_AUTH_CLIENT_ALLOWED_ORIGIN,
  databaseHooks: {
    user: {
      create: {
        after: async (user) => {
          const userEmail = user.email;
          const [_, domain] = userEmail.split("@");
          const [companyName] = domain.split(".");

          await auth.api.createOrganization({
            body: {
              name: companyName,
              slug: companyName,
              userId: user.id,
            },
          });
        },
      },
    },
  },
});

On client:

import type { BetterAuthClientPlugin } from "better-auth";
import { emailOTPClient, organizationClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";

const fastifyAdapterPlugin = () => {
  return {
    id: "next-cookies-request",
    fetchPlugins: [
      {
        id: "next-cookies-request-plugin",
        name: "next-cookies-request-plugin",
        hooks: {
          async onRequest(ctx) {
            const isServerSide = typeof window === "undefined";

            if (isServerSide) {
              const { cookies } = await import("next/headers");
              const headers = await cookies();
              ctx.headers.set("cookie", headers.toString());
            }
          },
        },
      },
    ],
  } satisfies BetterAuthClientPlugin;
};

const remoteUrlPlugin = () => {
  return {
    id: "remote-url",
    fetchPlugins: [
      {
        id: "remote-url-plugin",
        name: "remote-url-plugin",
        hooks: {
          async onRequest(ctx) {
            const isServerSide = typeof window === "undefined";

            if (isServerSide) {
              console.log('server')
              ctx.url = `${process.env.NEXT_PUBLIC_API_URL}${ctx.url}`;
              console.log('ctx.url', ctx.url);
            }
          },
        },
      },
    ],
  } satisfies BetterAuthClientPlugin;
};

export const authClient = createAuthClient({
  plugins: [emailOTPClient(), organizationClient(), fastifyAdapterPlugin(), remoteUrlPlugin()],
});

Additional context

Additional next config to make the separate backend works:

rewrites: async () => {
    return [
      {
        source: "/api/auth/:path*",
        destination: `${process.env.NEXT_PUBLIC_API_URL}/api/auth/:path*`,
      },
    ];
  },

My client lives on http//app.localhost:3002/login and my server on http://localhost:3333

Originally created by @felipinas on GitHub (Nov 13, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5951 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Ex: 1. Create a server in Fastify 2. Create a client in NextJS 3. Create the Google OAuth config 4. Try to sign in with this provider 5. The cookie is not set on browser ### Current vs. Expected behavior Following the steps from above, I expected to store the cookies in the browser as well, but I just can only do it if I use an "internal" auth method, like OTP. It seems like some redirection or setup error, I dunno. ### What version of Better Auth are you using? 1.3.13 (Downgraded to this one cause I was getting a `state_mismatch`) ### System info ```bash "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:29:54 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8122", "release": "24.6.0", "cpuCount": 8, "cpuModel": "Apple M3", "totalMemory": "16.00 GB", "freeMemory": "0.09 GB" }, "node": { "version": "v22.20.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.3" }, "frameworks": [ { "name": "fastify", "version": "^5.6.1" } ], "databases": [ { "name": "@prisma/client", "version": "^6.19.0" } ], } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript On fastify: import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { emailOTP, openAPI, organization } from "better-auth/plugins"; import { env } from "@/env"; import { prisma } from "@/lib/db"; export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", }), plugins: [ emailOTP({ async sendVerificationOTP({ email, otp, type }) { console.log("Sending OTP to", email, otp, type); if (type === "sign-in") { // Send the OTP for sign in } else if (type === "email-verification") { // Send the OTP for email verification } else { // Send the OTP for password reset } }, }), openAPI(), organization(), ], socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }, }, trustedOrigins: env.BETTER_AUTH_CLIENT_ALLOWED_ORIGIN, databaseHooks: { user: { create: { after: async (user) => { const userEmail = user.email; const [_, domain] = userEmail.split("@"); const [companyName] = domain.split("."); await auth.api.createOrganization({ body: { name: companyName, slug: companyName, userId: user.id, }, }); }, }, }, }, }); On client: import type { BetterAuthClientPlugin } from "better-auth"; import { emailOTPClient, organizationClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; const fastifyAdapterPlugin = () => { return { id: "next-cookies-request", fetchPlugins: [ { id: "next-cookies-request-plugin", name: "next-cookies-request-plugin", hooks: { async onRequest(ctx) { const isServerSide = typeof window === "undefined"; if (isServerSide) { const { cookies } = await import("next/headers"); const headers = await cookies(); ctx.headers.set("cookie", headers.toString()); } }, }, }, ], } satisfies BetterAuthClientPlugin; }; const remoteUrlPlugin = () => { return { id: "remote-url", fetchPlugins: [ { id: "remote-url-plugin", name: "remote-url-plugin", hooks: { async onRequest(ctx) { const isServerSide = typeof window === "undefined"; if (isServerSide) { console.log('server') ctx.url = `${process.env.NEXT_PUBLIC_API_URL}${ctx.url}`; console.log('ctx.url', ctx.url); } }, }, }, ], } satisfies BetterAuthClientPlugin; }; export const authClient = createAuthClient({ plugins: [emailOTPClient(), organizationClient(), fastifyAdapterPlugin(), remoteUrlPlugin()], }); ``` ### Additional context Additional next config to make the separate backend works: ``` rewrites: async () => { return [ { source: "/api/auth/:path*", destination: `${process.env.NEXT_PUBLIC_API_URL}/api/auth/:path*`, }, ]; }, ``` My client lives on http//app.localhost:3002/login and my server on http://localhost:3333
GiteaMirror added the locked label 2026-04-17 18:52:48 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 13, 2025):

It looks like your Fastify backend may not be sending or configuring the cookie as expected for cross-domain authentication when using Google OAuth. Here are a few steps you can try:

• Verify that your trusted origins (and any CORS settings on your Fastify server) include the client URL (in your case http://app.localhost:3002) so that cookies are accepted by the browser. In your config you’ve set trustedOrigins, so double-check that these match exactly the domain and protocol used in your browser [1].

• Ensure that the cookie attributes (such as SameSite, Secure, and Domain) are properly configured. Cookies coming from your backend may need SameSite set to “none” and Secure enabled if you are working across different origins. You might need to adjust these settings in your Fastify cookie configuration so that browsers accept the cookie from a different domain [2].

• Confirm that your rewrites and remote URL plugin are correctly forwarding requests and headers. Misrouted requests could lead to missing cookies. Since your client lives on a separate port (and possibly domain) than your backend, please double-check that the forwarded API calls include the cookies in both directions [3].

Have you tried logging the response headers from the backend to see if the set-cookie header is properly being sent? Tag @better-auth-agent if you would like an answer from the Agent. What has been your experience with the cookie headers during debugging?

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3525202829 --> @better-auth-agent[bot] commented on GitHub (Nov 13, 2025): It looks like your Fastify backend may not be sending or configuring the cookie as expected for cross-domain authentication when using Google OAuth. Here are a few steps you can try: • Verify that your trusted origins (and any CORS settings on your Fastify server) include the client URL (in your case http://app.localhost:3002) so that cookies are accepted by the browser. In your config you’ve set trustedOrigins, so double-check that these match exactly the domain and protocol used in your browser [[1]](https://github.com/better-auth/better-auth/issues/2285). • Ensure that the cookie attributes (such as SameSite, Secure, and Domain) are properly configured. Cookies coming from your backend may need SameSite set to “none” and Secure enabled if you are working across different origins. You might need to adjust these settings in your Fastify cookie configuration so that browsers accept the cookie from a different domain [[2]](https://github.com/better-auth/better-auth/issues/3672). • Confirm that your rewrites and remote URL plugin are correctly forwarding requests and headers. Misrouted requests could lead to missing cookies. Since your client lives on a separate port (and possibly domain) than your backend, please double-check that the forwarded API calls include the cookies in both directions [[3]](https://www.better-auth.com/). Have you tried logging the response headers from the backend to see if the set-cookie header is properly being sent? Tag @better-auth-agent if you would like an answer from the Agent. What has been your experience with the cookie headers during debugging? <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@felipinas commented on GitHub (Nov 13, 2025):

I have this fastify plugin to manage better-auth handler and I logged the headers from response:

import { auth } from "@/lib/auth";
import type { FastifyTypedInstance } from "@/types";

export const authConfig = (app: FastifyTypedInstance) => {
  app.route({
    method: ["GET", "POST"],
    url: "/api/auth/*",
    async handler(request, reply) {
      try {
        // Construct request URL
        const url = new URL(request.url, `http://${request.headers.host}`);

        // Convert Fastify headers to standard Headers object
        const headers = new Headers();
        Object.entries(request.headers).forEach(([key, value]) => {
          if (value) headers.append(key, value.toString());
        });
        // Create Fetch API-compatible request
        const req = new Request(url.toString(), {
          method: request.method,
          headers,
          body: request.body ? JSON.stringify(request.body) : undefined,
        });
        // Process authentication request
        const response = await auth.handler(req);
        // Forward response to client
        reply.status(response.status);

        console.log(response.headers);

        response.headers.forEach((value, key) => reply.header(key, value));

        reply.send(response.body ? await response.text() : null);
      } catch (error) {
        app.log.error("Authentication Error:", error);
        reply.status(500).send({
          error: "Internal authentication error",
          code: "AUTH_FAILURE",
        });
      }
    },
  });
};

That's the output:

Headers {
  'set-cookie': 'better-auth.session_token=A_VALID_TOKEN; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax',
  location: 'http://app.localhost:3002',
  'content-type': 'application/json'
<!-- gh-comment-id:3525290289 --> @felipinas commented on GitHub (Nov 13, 2025): I have this fastify plugin to manage better-auth handler and I logged the headers from response: ```js import { auth } from "@/lib/auth"; import type { FastifyTypedInstance } from "@/types"; export const authConfig = (app: FastifyTypedInstance) => { app.route({ method: ["GET", "POST"], url: "/api/auth/*", async handler(request, reply) { try { // Construct request URL const url = new URL(request.url, `http://${request.headers.host}`); // Convert Fastify headers to standard Headers object const headers = new Headers(); Object.entries(request.headers).forEach(([key, value]) => { if (value) headers.append(key, value.toString()); }); // Create Fetch API-compatible request const req = new Request(url.toString(), { method: request.method, headers, body: request.body ? JSON.stringify(request.body) : undefined, }); // Process authentication request const response = await auth.handler(req); // Forward response to client reply.status(response.status); console.log(response.headers); response.headers.forEach((value, key) => reply.header(key, value)); reply.send(response.body ? await response.text() : null); } catch (error) { app.log.error("Authentication Error:", error); reply.status(500).send({ error: "Internal authentication error", code: "AUTH_FAILURE", }); } }, }); }; ``` That's the output: ``` Headers { 'set-cookie': 'better-auth.session_token=A_VALID_TOKEN; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax', location: 'http://app.localhost:3002', 'content-type': 'application/json' ```
Author
Owner

@felipinas commented on GitHub (Nov 13, 2025):

Image

With internal methods (OTP), it's working

<!-- gh-comment-id:3525354306 --> @felipinas commented on GitHub (Nov 13, 2025): <img width="1072" height="190" alt="Image" src="https://github.com/user-attachments/assets/28938fc6-b3eb-417a-8171-10700558918c" /> With internal methods (OTP), it's working
Author
Owner

@rovertrack commented on GitHub (Nov 13, 2025):

so it must work without OTP too right.

<!-- gh-comment-id:3526916046 --> @rovertrack commented on GitHub (Nov 13, 2025): so it must work without OTP too right.
Author
Owner

@felipinas commented on GitHub (Nov 13, 2025):

@rovertrack it's not working, that's the problem :(

<!-- gh-comment-id:3527814398 --> @felipinas commented on GitHub (Nov 13, 2025): @rovertrack it's not working, that's the problem :(
Author
Owner

@felipinas commented on GitHub (Nov 14, 2025):

Closing this issue.

I noticed it was related to the different domains I was trying to use. app.localhost:3002 for FE and localhost:3333 for BE.

I ended up setting the redirectURI to my frontend, and it's working fine if I don't use subdomains and take advantage of Next rewrites.

<!-- gh-comment-id:3532946937 --> @felipinas commented on GitHub (Nov 14, 2025): Closing this issue. I noticed it was related to the different domains I was trying to use. app.localhost:3002 for FE and localhost:3333 for BE. I ended up setting the redirectURI to my frontend, and it's working fine if I don't use subdomains and take advantage of Next rewrites.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27707