oAuthProxy plugin doesn't work #2034

Closed
opened 2026-03-13 09:22:13 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @nikita030493 on GitHub (Sep 27, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

When initializing auth flow with oAuthProxy plugin enabled from localhost or any preview environment hosted in Vercel (random url generated for each PR and commit), better-auth is unable to correctly run through any configured third party auth flows: Google, Github.
⚠️ Both auth flow work just fine in production.

After debugging for many hours and trying different fixes I was unable to find a solution.
AI suggests that the problem is in better-auth proxy implementation where it fails to persist the temporary PKCE verification data (the state and code_challenge) in non-prod environment from which auth flow is initiated. Therefore prod environment is unable to verify the state. But, I'm not sure if it's accurate.

When using Google auth, initiated from localhost:

I can run through login flow, but in the end I get the following screen generated by better-auth in production domain:
Image
with following error from Vercel logs running in production env (state value omitted):
ERROR [Better Auth]: State Mismatch. Verification not found { state: '<sensetive_state_value>' }.
I don't get redirected back to localhost.

When using Github auth, initiated from localhost:

Github doesn't even go through login flow, redirecting to the warning page:
Image

When using Google OR Github auth, initiated from preview Vercel domain:

Vercel preview env deploys at https://my-app-*-my-project-*.vercel.app

My Configuration:

nextjs: 15.5.4
better-auth: 1.3.18
drizzle: 0.44.3
@neondatabase/serverless: 1.0.1

  • production and preview environments hosted on Vercel
  • BETTER_AUTH_URL is set to http://localhost:3000 locally and to https://my-app-domain.vercel.app in preview and production environments
  • BETTER_AUTH_SECRET is set to same value across ALL environments including local.
  • Both Google and Github oAuth clients configured properly.

Better-auth configuration

export const auth = betterAuth({
  trustedOrigins: [
    "http://localhost:3000",
    "https://my-app-*-my-project-*.vercel.app",
    "https://my-app-domain.vercel.app",
  ],
  database: drizzleAdapter(drizzleDb, {
    provider: "pg",
  }),
  socialProviders: {
    github: {
      ...github, // client id and secret
      redirectURI: "https://my-app-domain.vercel.app/api/auth/callback/github",
    },
    google: {
      ...google, // client id and secret
      accessType: "offline",
      prompt: "select_account consent",
      redirectURI: "https://my-app-domain.vercel.app/api/auth/callback/google",
    },
  },
  plugins: [oAuthProxy({ productionURL: "https://my-app-domain.vercel.app" }), nextCookies()],
});

Generated & applied better-auth schema for Drizzle:

export const user = authSchema.table("user", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  emailVerified: boolean("email_verified").default(false).notNull(),
  image: text("image"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .defaultNow()
    .$onUpdate(() => /* @__PURE__ */ new Date())
    .notNull(),
});

export const session = authSchema.table("session", {
  id: text("id").primaryKey(),
  expiresAt: timestamp("expires_at").notNull(),
  token: text("token").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .$onUpdate(() => /* @__PURE__ */ new Date())
    .notNull(),
  ipAddress: text("ip_address"),
  userAgent: text("user_agent"),
  userId: text("user_id")
    .notNull()
    .references(() => user.id, { onDelete: "cascade" }),
});

export const account = authSchema.table("account", {
  id: text("id").primaryKey(),
  accountId: text("account_id").notNull(),
  providerId: text("provider_id").notNull(),
  userId: text("user_id")
    .notNull()
    .references(() => user.id, { onDelete: "cascade" }),
  accessToken: text("access_token"),
  refreshToken: text("refresh_token"),
  idToken: text("id_token"),
  accessTokenExpiresAt: timestamp("access_token_expires_at"),
  refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
  scope: text("scope"),
  password: text("password"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .$onUpdate(() => /* @__PURE__ */ new Date())
    .notNull(),
});

export const verification = authSchema.table("verification", {
  id: text("id").primaryKey(),
  identifier: text("identifier").notNull(),
  value: text("value").notNull(),
  expiresAt: timestamp("expires_at").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at")
    .defaultNow()
    .$onUpdate(() => /* @__PURE__ */ new Date())
    .notNull(),
});

nextjs middleware:

export const config = {
  matcher: ["/", "/profile", "/items"],
  runtime: "nodejs",
};

export async function middleware(request: NextRequest) {
  const authSession = await auth.api.getSession({
    headers: request.headers,
  });
  if (!authSession?.session) {
    console.log("Unauthorized, redirecting to /login");
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

Login component:

"use client";
import { createAuthClient } from "better-auth/client";

export default function Login() {
  const authClient = createAuthClient();
  return (
    <div>
      <button
        onClick={async () => {
          await authClient.signIn.social({
            provider: "google",
            callbackURL: "/",
          });
        }}
      >
        Sign in with Google
      </button>

      <button
        onClick={async () => {
          await authClient.signIn.social({
            provider: "github",
            callbackURL: "/",
          });
        }}
      >
        Sign in with Github
      </button>
    </div>
  );
}

Current vs. Expected behavior

Expected:
Be able to run third party auth flow in non production environments through deployed app configured with better-auth in production.

Current:
Third party auth doesn't work from any environment except for production.

What version of Better Auth are you using?

1.3.18

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:54 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T6041",
    "release": "25.0.0",
    "cpuCount": 14,
    "cpuModel": "Apple M4 Pro",
    "totalMemory": "48.00 GB",
    "freeMemory": "5.22 GB"
  },
  "node": {
    "version": "v22.14.0",
    "env": "development"
  },
  "packageManager": {
    "name": "pnpm",
    "version": "10.17.1"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "15.5.4"
    },
    {
      "name": "react",
      "version": "19.1.1"
    }
  ],
  "databases": [
    {
      "name": "drizzle",
      "version": "^0.44.3"
    }
  ],
  "betterAuth": {
    "version": "^1.3.18",
    "config": null
  }
}

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

Client

Auth config (if applicable)


Additional context

No response

Originally created by @nikita030493 on GitHub (Sep 27, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce When initializing auth flow with oAuthProxy plugin enabled from localhost or any preview environment hosted in Vercel (random url generated for each PR and commit), better-auth is unable to correctly run through any configured third party auth flows: Google, Github. ⚠️ Both auth flow work just fine in production. After debugging for many hours and trying different fixes I was unable to find a solution. AI suggests that the problem is in better-auth proxy implementation where it fails to persist the temporary PKCE verification data (the state and code_challenge) in non-prod environment from which auth flow is initiated. Therefore prod environment is unable to verify the state. But, I'm not sure if it's accurate. ## When using Google auth, initiated from `localhost`: I can run through login flow, but in the end I get the following screen generated by better-auth in production domain: <img width="429" height="394" alt="Image" src="https://github.com/user-attachments/assets/806a7f57-cafe-4e6c-9376-df0069832fd2" /> with following error from Vercel logs running in production env (state value omitted): `ERROR [Better Auth]: State Mismatch. Verification not found { state: '<sensetive_state_value>' }`. I don't get redirected back to localhost. ## When using Github auth, initiated from `localhost`: Github doesn't even go through login flow, redirecting to the warning page: <img width="672" height="318" alt="Image" src="https://github.com/user-attachments/assets/6c32d930-1101-4df5-b9f4-cec8f84cd6ad" /> ## When using Google OR Github auth, initiated from preview Vercel domain: Vercel preview env deploys at `https://my-app-*-my-project-*.vercel.app` - Login flow initiates at https://my-app-*-my-project-*.vercel.app - I end up being logged in and redirected to production domain instead: https://my-app-domain.vercel.app - When navigating back to https://my-app-*-my-project-*.vercel.app, I'm redirected to login as there is no session in this domain. ## My Configuration: nextjs: 15.5.4 better-auth: 1.3.18 drizzle: 0.44.3 @neondatabase/serverless: 1.0.1 - production and preview environments hosted on Vercel - BETTER_AUTH_URL is set to `http://localhost:3000` locally and to `https://my-app-domain.vercel.app` in preview and production environments - BETTER_AUTH_SECRET is set to same value across ALL environments including local. - Both Google and Github oAuth clients configured properly. ## Better-auth configuration ```ts export const auth = betterAuth({ trustedOrigins: [ "http://localhost:3000", "https://my-app-*-my-project-*.vercel.app", "https://my-app-domain.vercel.app", ], database: drizzleAdapter(drizzleDb, { provider: "pg", }), socialProviders: { github: { ...github, // client id and secret redirectURI: "https://my-app-domain.vercel.app/api/auth/callback/github", }, google: { ...google, // client id and secret accessType: "offline", prompt: "select_account consent", redirectURI: "https://my-app-domain.vercel.app/api/auth/callback/google", }, }, plugins: [oAuthProxy({ productionURL: "https://my-app-domain.vercel.app" }), nextCookies()], }); ``` ## Generated & applied better-auth schema for Drizzle: ```ts export const user = authSchema.table("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const session = authSchema.table("session", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }); export const account = authSchema.table("account", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); export const verification = authSchema.table("verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: timestamp("expires_at").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), }); ``` ## nextjs middleware: ```ts export const config = { matcher: ["/", "/profile", "/items"], runtime: "nodejs", }; export async function middleware(request: NextRequest) { const authSession = await auth.api.getSession({ headers: request.headers, }); if (!authSession?.session) { console.log("Unauthorized, redirecting to /login"); return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next(); } ``` ## Login component: ```ts "use client"; import { createAuthClient } from "better-auth/client"; export default function Login() { const authClient = createAuthClient(); return ( <div> <button onClick={async () => { await authClient.signIn.social({ provider: "google", callbackURL: "/", }); }} > Sign in with Google </button> <button onClick={async () => { await authClient.signIn.social({ provider: "github", callbackURL: "/", }); }} > Sign in with Github </button> </div> ); } ``` ### Current vs. Expected behavior Expected: Be able to run third party auth flow in non production environments through deployed app configured with better-auth in production. Current: Third party auth doesn't work from any environment except for production. ### What version of Better Auth are you using? 1.3.18 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:54 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T6041", "release": "25.0.0", "cpuCount": 14, "cpuModel": "Apple M4 Pro", "totalMemory": "48.00 GB", "freeMemory": "5.22 GB" }, "node": { "version": "v22.14.0", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.17.1" }, "frameworks": [ { "name": "next", "version": "15.5.4" }, { "name": "react", "version": "19.1.1" } ], "databases": [ { "name": "drizzle", "version": "^0.44.3" } ], "betterAuth": { "version": "^1.3.18", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the nextjs label 2026-03-13 09:22:13 -05:00
Author
Owner

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

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

Your issue is likely caused by a combination of redirect URI and callbackURL configuration, as well as a known bug in the oAuthProxy plugin that was fixed after your version.

For Google, you must register both your production and local callback URLs in the Google Cloud Console (e.g., http://localhost:3000/api/auth/callback/google and https://my-app-domain.vercel.app/api/auth/callback/google)—otherwise, you'll get state mismatch errors because the OAuth flow can't complete properly across environments [source].

For GitHub, the callbackURL you pass to signIn.social should be a user-facing route (like /), not the /api/auth/callback/github endpoint. The GitHub OAuth app's redirect URI should still be set to /api/auth/callback/github [source].

The oAuthProxy plugin is designed to let you initiate auth from non-production environments and complete the flow in production, but in your version (1.3.18), there was a bug where the proxy logic didn't always use the correct production URL for origin comparison. This could cause the state/cookie handoff to break, resulting in the "State Mismatch. Verification not found" error. This was fixed in a recent release—upgrading better-auth to the latest version will ensure the proxy uses the productionURL option correctly and handles preview/local flows as intended [source].

Make sure:

  • All possible preview/local URLs are included in trustedOrigins.
  • Your OAuth provider (Google, GitHub) has all relevant callback URLs registered.
  • You use the latest better-auth version.
  • You set the productionURL option in oAuthProxy to your production domain.

If you still see issues after upgrading and correcting callback URLs, double-check that your preview/local environments share the same database as production (for verification state), and that cookies are set and passed correctly across redirects.

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 (Sep 27, 2025): <!-- Greeting --> Hi @nikita030493! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your issue is likely caused by a combination of redirect URI and callbackURL configuration, as well as a known bug in the oAuthProxy plugin that was fixed after your version. For Google, you must register both your production and local callback URLs in the Google Cloud Console (e.g., `http://localhost:3000/api/auth/callback/google` and `https://my-app-domain.vercel.app/api/auth/callback/google`)—otherwise, you'll get state mismatch errors because the OAuth flow can't complete properly across environments [[source]](https://github.com/better-auth/better-auth/issues/3062). For GitHub, the callbackURL you pass to `signIn.social` should be a user-facing route (like `/`), not the `/api/auth/callback/github` endpoint. The GitHub OAuth app's redirect URI should still be set to `/api/auth/callback/github` [[source]](https://github.com/better-auth/better-auth/issues/3488). The oAuthProxy plugin is designed to let you initiate auth from non-production environments and complete the flow in production, but in your version (1.3.18), there was a bug where the proxy logic didn't always use the correct production URL for origin comparison. This could cause the state/cookie handoff to break, resulting in the "State Mismatch. Verification not found" error. This was fixed in a recent release—upgrading better-auth to the latest version will ensure the proxy uses the `productionURL` option correctly and handles preview/local flows as intended [[source]](https://github.com/better-auth/better-auth/pull/4825). Make sure: - All possible preview/local URLs are included in `trustedOrigins`. - Your OAuth provider (Google, GitHub) has all relevant callback URLs registered. - You use the latest better-auth version. - You set the `productionURL` option in oAuthProxy to your production domain. If you still see issues after upgrading and correcting callback URLs, double-check that your preview/local environments share the same database as production (for verification state), and that cookies are set and passed correctly across redirects. <!-- 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/e6d714f6-8660-4759-8a82-5ffb45271c19?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/e6d714f6-8660-4759-8a82-5ffb45271c19?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/e6d714f6-8660-4759-8a82-5ffb45271c19?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/e6d714f6-8660-4759-8a82-5ffb45271c19?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/e6d714f6-8660-4759-8a82-5ffb45271c19?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/e6d714f6-8660-4759-8a82-5ffb45271c19?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/e6d714f6-8660-4759-8a82-5ffb45271c19?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/4953)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2034