[GH-ISSUE #5034] expo - state_mismatch error for google #10147

Closed
opened 2026-04-13 06:05:12 -05:00 by GiteaMirror · 31 comments
Owner

Originally created by @fdemir on GitHub (Oct 2, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5034

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Use the following setup and sign in using authClign.signIn.social. Nothing different.

Current vs. Expected behavior

Current:
BetterAuth gives Error Code: state_mismatch.

Expected behavior:
User should be redirect to the app.

What version of Better Auth are you using?

1.2.12

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.4.0: Fri Apr 11 18:33:47 PDT 2025; root:xnu-11417.101.15~117/RELEASE_ARM64_T6030",
    "release": "24.4.0",
    "cpuCount": 11,
    "cpuModel": "Apple M3 Pro",
    "totalMemory": "18.00 GB",
    "freeMemory": "0.24 GB"
  },
  "node": {
    "version": "v20.12.2",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.5.0"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }

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

Backend, Client

Auth config (if applicable)

import { expo } from "@better-auth/expo";
import { prisma } from "@wac/db";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { magicLink } from "better-auth/plugins";
import { Resend } from "resend";

const isProduction = process.env.NODE_ENV === "production";

const resend = new Resend(process.env.RESEND_API_KEY);

export const auth = betterAuth({
  trustedOrigins: [
    "yourapp://",
    "https://yourapp.in",
    "https://www.yourapp.com",
  ],
  advanced: {
    ...(isProduction && {
      crossSubDomainCookies: {
        enabled: true,
        domain: ".yourapp.com",
      },
    }),
  },
  plugins: [
    expo({}),
    magicLink({
      sendMagicLink: async ({ email, token, url }, request) => {
        await resend.emails.send({
          from: "noreply@yourapp.com",
          to: email,
          subject: "Login to YourApp",
          html: `<p>Login to your account with this link: <a href="${url}">Click here to sign in</a></p>`,
        });
      },
    }),
  ],
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  emailAndPassword: {
    enabled: false,
  },
  cookieCache: {
    enabled: true,
    maxAge: 5 * 60, // Cache duration in seconds
  },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
    apple: {
      clientId: process.env.AUTH_APPLE_ID!,
      clientSecret: process.env.AUTH_APPLE_SECRET!,
      // TODO: Add in.yourapp.app to the prod envs
      appBundleIdentifier:
        process.env.APPLE_BUNDLE_IDENTIFIER || "host.exp.Exponent",
    },
  },
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 10 * 60,
    },
    fields: {
      expiresAt: "expires", // Map your existing `expires` field to Better Auth's `expiresAt`
      token: "sessionToken", // Map your existing `sessionToken` field to Better Auth's `token`
    },
  },
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ["google", "apple"],
      allowDifferentEmails: true,
    },
    fields: {
      accountId: "providerAccountId",
      refreshToken: "refresh_token",
      accessToken: "access_token",
      // accessTokenExpiresAt: "access_token_expires",
      idToken: "id_token",
      providerId: "provider",
    },
  },
  user: {
    additionalFields: {
      plan: {
        type: "string",
        required: false,
        defaultValue: "FREE",
        input: false,
      },
      watchRegionCode: {
        type: "string",
        required: false,
        defaultValue: "US",
        input: false,
      },
    },
  },
  onAPIError: {
    onError(error, ctx) {
      console.log(error, ctx);
    },
  },
  logger: {
    level: "debug",
    log(level, message, args) {
      console.log(level, message, ...args);
    },
  },
});

export type AuthType = {
  user: typeof auth.$Infer.Session.user | null;
  session: typeof auth.$Infer.Session.session | null;
};

Additional context

I have an application which runs web and mobile using better auth for authentication. on web, users can successfully login their acconts using google. however, new or existing users can't login using google on ios. my implementation is very straightforward.

additional info:

  • my api works under subdomain, api.*
  • social sign in works on web, not on mobile!!!!
Image
  • checked the authorized redirect URIs, all correct
  • checked logs, couldnt find anything helpful
Originally created by @fdemir on GitHub (Oct 2, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5034 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Use the following setup and sign in using `authClign.signIn.social`. Nothing different. ### Current vs. Expected behavior Current: BetterAuth gives `Error Code: state_mismatch`. Expected behavior: User should be redirect to the app. ### What version of Better Auth are you using? 1.2.12 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.4.0: Fri Apr 11 18:33:47 PDT 2025; root:xnu-11417.101.15~117/RELEASE_ARM64_T6030", "release": "24.4.0", "cpuCount": 11, "cpuModel": "Apple M3 Pro", "totalMemory": "18.00 GB", "freeMemory": "0.24 GB" }, "node": { "version": "v20.12.2", "env": "development" }, "packageManager": { "name": "npm", "version": "10.5.0" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import { expo } from "@better-auth/expo"; import { prisma } from "@wac/db"; import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { magicLink } from "better-auth/plugins"; import { Resend } from "resend"; const isProduction = process.env.NODE_ENV === "production"; const resend = new Resend(process.env.RESEND_API_KEY); export const auth = betterAuth({ trustedOrigins: [ "yourapp://", "https://yourapp.in", "https://www.yourapp.com", ], advanced: { ...(isProduction && { crossSubDomainCookies: { enabled: true, domain: ".yourapp.com", }, }), }, plugins: [ expo({}), magicLink({ sendMagicLink: async ({ email, token, url }, request) => { await resend.emails.send({ from: "noreply@yourapp.com", to: email, subject: "Login to YourApp", html: `<p>Login to your account with this link: <a href="${url}">Click here to sign in</a></p>`, }); }, }), ], database: prismaAdapter(prisma, { provider: "postgresql", }), emailAndPassword: { enabled: false, }, cookieCache: { enabled: true, maxAge: 5 * 60, // Cache duration in seconds }, socialProviders: { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, apple: { clientId: process.env.AUTH_APPLE_ID!, clientSecret: process.env.AUTH_APPLE_SECRET!, // TODO: Add in.yourapp.app to the prod envs appBundleIdentifier: process.env.APPLE_BUNDLE_IDENTIFIER || "host.exp.Exponent", }, }, session: { cookieCache: { enabled: true, maxAge: 10 * 60, }, fields: { expiresAt: "expires", // Map your existing `expires` field to Better Auth's `expiresAt` token: "sessionToken", // Map your existing `sessionToken` field to Better Auth's `token` }, }, account: { accountLinking: { enabled: true, trustedProviders: ["google", "apple"], allowDifferentEmails: true, }, fields: { accountId: "providerAccountId", refreshToken: "refresh_token", accessToken: "access_token", // accessTokenExpiresAt: "access_token_expires", idToken: "id_token", providerId: "provider", }, }, user: { additionalFields: { plan: { type: "string", required: false, defaultValue: "FREE", input: false, }, watchRegionCode: { type: "string", required: false, defaultValue: "US", input: false, }, }, }, onAPIError: { onError(error, ctx) { console.log(error, ctx); }, }, logger: { level: "debug", log(level, message, args) { console.log(level, message, ...args); }, }, }); export type AuthType = { user: typeof auth.$Infer.Session.user | null; session: typeof auth.$Infer.Session.session | null; }; ``` ### Additional context I have an application which runs web and mobile using better auth for authentication. on web, users can successfully login their acconts using google. however, new or existing users can't login using google on ios. my implementation is very straightforward. additional info: - my api works under subdomain, api.* - social sign in works on web, not on mobile!!!! <img width="368" height="549" alt="Image" src="https://github.com/user-attachments/assets/7e19a2d9-822e-4fc3-9dca-7ff131a05f01" /> - checked the authorized redirect URIs, all correct - checked logs, couldnt find anything helpful
GiteaMirror added the locked label 2026-04-13 06:05:12 -05:00
Author
Owner

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

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

The state_mismatch error with Google login on Expo/iOS is usually caused by issues with how the OAuth state parameter is stored and retrieved during the authentication flow. On mobile, cookies and session storage behave differently than on web, so Expo-specific handling is required.

The @better-auth/expo plugin tries to work around this by propagating session cookies via URL query parameters during OAuth redirects, especially for trusted origins like exp:// in development. If this mechanism fails, the server can't validate the state parameter, resulting in a CSRF protection error state_mismatch.

Common causes and fixes:

  • Redirect URI configuration: For mobile, Google does not accept localhost or private IPs as redirect URIs. Use a tunneling service like ngrok to expose your backend with a public HTTPS URL, and register this as the redirect URI in both Google Cloud Console and your Better Auth config redirect URI advice.
  • Trusted origins: Make sure your Better Auth config includes the correct trusted origins for your app scheme (e.g., exp://, yourapp://).
  • Session/cookie propagation: Ensure your Expo client correctly extracts the session cookie from the redirect URL and sets it for subsequent API requests. If you’re not handling this in your client, the session won’t persist and the state won’t match.
  • Version differences: Some workarounds for state/cookie handling changed in newer Better Auth versions. If you upgrade, check the changelog for breaking changes in OAuth state management.

If you’ve already checked redirect URIs and trusted origins, focus on how your Expo client handles the OAuth callback and session cookie. You may need to manually parse the cookie from the redirect URL and set it for your API requests to maintain session continuity.

References:

If you need more specific help, share how your Expo client handles the OAuth callback and session cookie after redirect.

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:3359222031 --> @dosubot[bot] commented on GitHub (Oct 2, 2025): <!-- Greeting --> Hi @fdemir! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The `state_mismatch` error with Google login on Expo/iOS is usually caused by issues with how the OAuth `state` parameter is stored and retrieved during the authentication flow. On mobile, cookies and session storage behave differently than on web, so Expo-specific handling is required. The `@better-auth/expo` plugin tries to work around this by propagating session cookies via URL query parameters during OAuth redirects, especially for trusted origins like `exp://` in development. If this mechanism fails, the server can't validate the `state` parameter, resulting in a CSRF protection error [`state_mismatch`](https://github.com/better-auth/better-auth/issues/4969). Common causes and fixes: - **Redirect URI configuration:** For mobile, Google does not accept localhost or private IPs as redirect URIs. Use a tunneling service like ngrok to expose your backend with a public HTTPS URL, and register this as the redirect URI in both Google Cloud Console and your Better Auth config [`redirect URI advice`](https://github.com/better-auth/better-auth/issues/4070). - **Trusted origins:** Make sure your Better Auth config includes the correct trusted origins for your app scheme (e.g., `exp://`, `yourapp://`). - **Session/cookie propagation:** Ensure your Expo client correctly extracts the session cookie from the redirect URL and sets it for subsequent API requests. If you’re not handling this in your client, the session won’t persist and the state won’t match. - **Version differences:** Some workarounds for state/cookie handling changed in newer Better Auth versions. If you upgrade, check the changelog for breaking changes in OAuth state management. If you’ve already checked redirect URIs and trusted origins, focus on how your Expo client handles the OAuth callback and session cookie. You may need to manually parse the cookie from the redirect URL and set it for your API requests to maintain session continuity. References: - [Expo plugin cookie propagation](https://github.com/better-auth/better-auth/blob/f6cbdcc84ee5d2971fdcc8b23ff7c174f88cf45b/packages/expo/src/index.ts) - [Google OAuth provider implementation](https://github.com/better-auth/better-auth/blob/f6cbdcc84ee5d2971fdcc8b23ff7c174f88cf45b/packages/better-auth/src/social-providers/google.ts) - [Generic OAuth state handling](https://github.com/better-auth/better-auth/blob/f6cbdcc84ee5d2971fdcc8b23ff7c174f88cf45b/packages/better-auth/src/plugins/generic-oauth/index.ts) - [Redirect URI setup for Expo](https://github.com/better-auth/better-auth/issues/4070) - [State mismatch troubleshooting](https://github.com/better-auth/better-auth/issues/4969) If you need more specific help, share how your Expo client handles the OAuth callback and session cookie after redirect. <!-- 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/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?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/5034)
Author
Owner

@fdemir commented on GitHub (Oct 2, 2025):

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

The state_mismatch error with Google login on Expo/iOS is usually caused by issues with how the OAuth state parameter is stored and retrieved during the authentication flow. On mobile, cookies and session storage behave differently than on web, so Expo-specific handling is required.

The @better-auth/expo plugin tries to work around this by propagating session cookies via URL query parameters during OAuth redirects, especially for trusted origins like exp:// in development. If this mechanism fails, the server can't validate the state parameter, resulting in a CSRF protection error state_mismatch.

Common causes and fixes:

  • Redirect URI configuration: For mobile, Google does not accept localhost or private IPs as redirect URIs. Use a tunneling service like ngrok to expose your backend with a public HTTPS URL, and register this as the redirect URI in both Google Cloud Console and your Better Auth config redirect URI advice.
  • Trusted origins: Make sure your Better Auth config includes the correct trusted origins for your app scheme (e.g., exp://, yourapp://).
  • Session/cookie propagation: Ensure your Expo client correctly extracts the session cookie from the redirect URL and sets it for subsequent API requests. If you’re not handling this in your client, the session won’t persist and the state won’t match.
  • Version differences: Some workarounds for state/cookie handling changed in newer Better Auth versions. If you upgrade, check the changelog for breaking changes in OAuth state management.

If you’ve already checked redirect URIs and trusted origins, focus on how your Expo client handles the OAuth callback and session cookie. You may need to manually parse the cookie from the redirect URL and set it for your API requests to maintain session continuity.

References:

If you need more specific help, share how your Expo client handles the OAuth callback and session cookie after redirect.

To reply, just mention @dosu.

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

here are the login and config part for the ios(client).

login.tsx

  const signInWithGoogle = async () => {
    await authClient.signIn.social({
      provider: "google",
      callbackURL: "/",
    });
}

auth-client.tsx

import { API_URL } from "@/constants/environment";
import { expoClient } from "@better-auth/expo/client";
import { inferAdditionalFields } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
  baseURL: API_URL + "/api/auth",
  plugins: [
    expoClient({
      scheme: "yourapp",
      storagePrefix: "yourapp",
      storage: SecureStore,
    }),
    inferAdditionalFields({
      user: {
        plan: {
          type: "string",
        },
        watchRegionCode: {
          type: "string",
        },
        watchRegionName: {
          type: "string",
        },
      },
    }),
  ],
});

declare module "better-auth/react" {
  interface Session {
    user: {
      plan: string;
    };
  }
}

@dosu

<!-- gh-comment-id:3359243047 --> @fdemir commented on GitHub (Oct 2, 2025): > Hi [@fdemir](https://github.com/fdemir)! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. > > The `state_mismatch` error with Google login on Expo/iOS is usually caused by issues with how the OAuth `state` parameter is stored and retrieved during the authentication flow. On mobile, cookies and session storage behave differently than on web, so Expo-specific handling is required. > > The `@better-auth/expo` plugin tries to work around this by propagating session cookies via URL query parameters during OAuth redirects, especially for trusted origins like `exp://` in development. If this mechanism fails, the server can't validate the `state` parameter, resulting in a CSRF protection error [`state_mismatch`](https://github.com/better-auth/better-auth/issues/4969). > > Common causes and fixes: > > * **Redirect URI configuration:** For mobile, Google does not accept localhost or private IPs as redirect URIs. Use a tunneling service like ngrok to expose your backend with a public HTTPS URL, and register this as the redirect URI in both Google Cloud Console and your Better Auth config [`redirect URI advice`](https://github.com/better-auth/better-auth/issues/4070). > * **Trusted origins:** Make sure your Better Auth config includes the correct trusted origins for your app scheme (e.g., `exp://`, `yourapp://`). > * **Session/cookie propagation:** Ensure your Expo client correctly extracts the session cookie from the redirect URL and sets it for subsequent API requests. If you’re not handling this in your client, the session won’t persist and the state won’t match. > * **Version differences:** Some workarounds for state/cookie handling changed in newer Better Auth versions. If you upgrade, check the changelog for breaking changes in OAuth state management. > > If you’ve already checked redirect URIs and trusted origins, focus on how your Expo client handles the OAuth callback and session cookie. You may need to manually parse the cookie from the redirect URL and set it for your API requests to maintain session continuity. > > References: > > * [Expo plugin cookie propagation](https://github.com/better-auth/better-auth/blob/f6cbdcc84ee5d2971fdcc8b23ff7c174f88cf45b/packages/expo/src/index.ts) > * [Google OAuth provider implementation](https://github.com/better-auth/better-auth/blob/f6cbdcc84ee5d2971fdcc8b23ff7c174f88cf45b/packages/better-auth/src/social-providers/google.ts) > * [Generic OAuth state handling](https://github.com/better-auth/better-auth/blob/f6cbdcc84ee5d2971fdcc8b23ff7c174f88cf45b/packages/better-auth/src/plugins/generic-oauth/index.ts) > * [Redirect URI setup for Expo](https://github.com/better-auth/better-auth/issues/4070) > * [State mismatch troubleshooting](https://github.com/better-auth/better-auth/issues/4969) > > If you need more specific help, share how your Expo client handles the OAuth callback and session cookie after redirect. > > _To reply, just mention [@dosu](https://go.dosu.dev/dosubot)._ > > How did I do? [Good](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/72a4d98c-d9c4-4116-a30c-6636bd2873cd?feedback_type=other)  [![Chat with Dosu](https://camo.githubusercontent.com/d9ffebb979729c9ef0fc4e3090dbae5b17cabbfec8afe378ada6b81913d6ad8a/68747470733a2f2f646f73752e6465762f646f73752d636861742d62616467652e737667)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github) [![Join Discord](https://camo.githubusercontent.com/d562c2df5e4567ea41d37f1d91a4da0839d8341037045b103b2da268998de17f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a6f696e2d3538363546323f6c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d)](https://go.dosu.dev/discord-bot) [![Share on X](https://camo.githubusercontent.com/4649f705e2396d6f328dbc8e800a0509c9c17b3b20253b36706c393df124b93d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f582d73686172652d626c61636b)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5034) here are the login and config part for the ios(client). login.tsx ``` const signInWithGoogle = async () => { await authClient.signIn.social({ provider: "google", callbackURL: "/", }); } ``` auth-client.tsx ``` import { API_URL } from "@/constants/environment"; import { expoClient } from "@better-auth/expo/client"; import { inferAdditionalFields } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/react"; import * as SecureStore from "expo-secure-store"; export const authClient = createAuthClient({ baseURL: API_URL + "/api/auth", plugins: [ expoClient({ scheme: "yourapp", storagePrefix: "yourapp", storage: SecureStore, }), inferAdditionalFields({ user: { plan: { type: "string", }, watchRegionCode: { type: "string", }, watchRegionName: { type: "string", }, }, }), ], }); declare module "better-auth/react" { interface Session { user: { plan: string; }; } } ```` @dosu
Author
Owner

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

Your Expo client config looks correct, but on Expo/iOS, session state after OAuth can be unreliable unless you explicitly refresh it. After calling authClient.signIn.social, try immediately calling await authClient.getSession() to force a session refresh—this is a known workaround for session not being available right after OAuth on mobile and helps avoid state_mismatch errors (reference).

Also, double-check that your Google Cloud Console and Better Auth backend both use a public HTTPS URL (e.g., via ngrok) as the redirect URI, not localhost or a custom scheme, and that your app scheme (e.g., yourapp://) is in trustedOrigins (reference).

If you still see the error, make sure your Expo client is using the authClient directly (not a custom API wrapper), as the plugin handles session cookie propagation and deep linking automatically (reference).

In summary:

  1. Use a public HTTPS redirect URI everywhere.
  2. Ensure your app scheme is in trustedOrigins.
  3. After sign-in, call await authClient.getSession() to refresh session state.

This should resolve the state_mismatch issue for Google login on Expo/iOS.

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:3359250725 --> @dosubot[bot] commented on GitHub (Oct 2, 2025): <!-- Answer --> Your Expo client config looks correct, but on Expo/iOS, session state after OAuth can be unreliable unless you explicitly refresh it. After calling `authClient.signIn.social`, try immediately calling `await authClient.getSession()` to force a session refresh—this is a known workaround for session not being available right after OAuth on mobile and helps avoid `state_mismatch` errors [(reference)](https://github.com/better-auth/better-auth/issues/3711). Also, double-check that your Google Cloud Console and Better Auth backend both use a public HTTPS URL (e.g., via ngrok) as the redirect URI, not localhost or a custom scheme, and that your app scheme (e.g., `yourapp://`) is in `trustedOrigins` [(reference)](https://github.com/better-auth/better-auth/issues/4070). If you still see the error, make sure your Expo client is using the `authClient` directly (not a custom API wrapper), as the plugin handles session cookie propagation and deep linking automatically [(reference)](https://github.com/better-auth/better-auth/issues/4969). In summary: 1. Use a public HTTPS redirect URI everywhere. 2. Ensure your app scheme is in `trustedOrigins`. 3. After sign-in, call `await authClient.getSession()` to refresh session state. This should resolve the `state_mismatch` issue for Google login on Expo/iOS. <!-- 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/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1652c4b0-cdba-4319-bc04-8f1f5f48bce2?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/5034)
Author
Owner

@fdemir commented on GitHub (Oct 2, 2025):

i couldn’t find the exact root cause, but i figured out why it happened. about a week ago, before the new version of my app was released, i ran expo install --fix. that command upgraded several packages, including expo and expo-router.

because of these version upgrades, the error occurred. it turned out that the better-auth version wasn’t compatible with the updated expo packages. after upgrading better-auth to the latest version, the issue was resolved.

<!-- gh-comment-id:3359310493 --> @fdemir commented on GitHub (Oct 2, 2025): i couldn’t find the exact root cause, but i figured out why it happened. about a week ago, before the new version of my app was released, i ran expo install --fix. that command upgraded several packages, including expo and expo-router. because of these version upgrades, the error occurred. it turned out that the better-auth version wasn’t compatible with the updated expo packages. after upgrading better-auth to the latest version, the issue was resolved.
Author
Owner

@zispidd commented on GitHub (Oct 2, 2025):

same issue

<!-- gh-comment-id:3360846125 --> @zispidd commented on GitHub (Oct 2, 2025): same issue
Author
Owner

@Bekacru commented on GitHub (Oct 2, 2025):

make sure @better-auth/expo is updated to latest same with better-auth

<!-- gh-comment-id:3361799130 --> @Bekacru commented on GitHub (Oct 2, 2025): make sure `@better-auth/expo` is updated to latest same with `better-auth`
Author
Owner

@zispidd commented on GitHub (Oct 2, 2025):

make sure @better-auth/expo is updated to latest same with better-auth

In my case, I don't use Better Auth Expo. I use bare React Native. When I call the login function via Google, I redirect the user to the browser using the link returned by the function so that they can log in there, after which they are redirected to the app from the web page.

<!-- gh-comment-id:3361828094 --> @zispidd commented on GitHub (Oct 2, 2025): > make sure `@better-auth/expo` is updated to latest same with `better-auth` In my case, I don't use Better Auth Expo. I use bare React Native. When I call the login function via Google, I redirect the user to the browser using the link returned by the function so that they can log in there, after which they are redirected to the app from the web page.
Author
Owner

@Bekacru commented on GitHub (Oct 2, 2025):

You need to add a version of this in your react native plugin/endpoint and make sure to use the proxy instead of directly redirecting the user to google. We now require state in cookies for all environments for security reasons, instead of overriding it internally. If you were using the plugin this should be handled automatically when you upgrade both better-auth and @better-auth/expo

73e1e2eaa2/packages/expo/src/index.ts (L86)

<!-- gh-comment-id:3362032486 --> @Bekacru commented on GitHub (Oct 2, 2025): You need to add a version of this in your react native plugin/endpoint and make sure to use the proxy instead of directly redirecting the user to google. We now require state in cookies for all environments for security reasons, instead of overriding it internally. If you were using the plugin this should be handled automatically when you upgrade both `better-auth` and `@better-auth/expo` https://github.com/better-auth/better-auth/blob/73e1e2eaa28fe46fe8a863a3e65f9ce9bd200d53/packages/expo/src/index.ts#L86
Author
Owner

@Bekacru commented on GitHub (Oct 2, 2025):

closing this issue, we'll add more information regarding this on the expo docs

<!-- gh-comment-id:3362780521 --> @Bekacru commented on GitHub (Oct 2, 2025): closing this issue, we'll add more information regarding this on the expo docs
Author
Owner

@bcheung commented on GitHub (Oct 14, 2025):

downgrading from 1.3.27 to 1.3.13 worked for me, not sure why

<!-- gh-comment-id:3404001002 --> @bcheung commented on GitHub (Oct 14, 2025): downgrading from 1.3.27 to 1.3.13 worked for me, not sure why
Author
Owner

@I0T-4-L1F3 commented on GitHub (Oct 23, 2025):

Why do you close this issue?

This is not only an expo issue.

My Szenario: Fastify Backend + Vite React Frontend

Social Login Error "state_missmatch" appears when I use better-auth 1.3.28

I tried a lot and the last try was downgrading to 1.3.13 like @bcheung said... But thats not a solution, thats a sign for "something is broke in the new versions".

<!-- gh-comment-id:3439213626 --> @I0T-4-L1F3 commented on GitHub (Oct 23, 2025): Why do you close this issue? This is not only an expo issue. My Szenario: Fastify Backend + Vite React Frontend Social Login Error "state_missmatch" appears when I use better-auth 1.3.28 I tried a lot and the last try was downgrading to 1.3.13 like @bcheung said... But thats not a solution, thats a sign for "something is broke in the new versions".
Author
Owner

@Bekacru commented on GitHub (Oct 23, 2025):

Are you using cookies for your app or storing tokens directly? @I0T-4-L1F3

<!-- gh-comment-id:3439226478 --> @Bekacru commented on GitHub (Oct 23, 2025): Are you using cookies for your app or storing tokens directly? @I0T-4-L1F3
Author
Owner

@imohitarora commented on GitHub (Oct 25, 2025):

I am having the same issue.

Backend - Hono.

Expo app, (works fine in ios and android simulator) fails with web.

version : - "better-auth": "^1.3.31"

<!-- gh-comment-id:3447771309 --> @imohitarora commented on GitHub (Oct 25, 2025): I am having the same issue. Backend - Hono. Expo app, (works fine in ios and android simulator) fails with web. version : - "better-auth": "^1.3.31"
Author
Owner

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

still have this issue on both 1.3.27 and 1.3.34

<!-- gh-comment-id:3499690637 --> @Mnigos commented on GitHub (Nov 6, 2025): still have this issue on both 1.3.27 and 1.3.34
Author
Owner

@Ash200042123 commented on GitHub (Nov 7, 2025):

having the same issue with better auth package version 1.3.25

my frontend and backend both are built in NextJs. I don't get any problem in localhost but in production I see in the logs I get this error sometimes

<!-- gh-comment-id:3501362042 --> @Ash200042123 commented on GitHub (Nov 7, 2025): having the same issue with better auth package version 1.3.25 my frontend and backend both are built in NextJs. I don't get any problem in localhost but in production I see in the logs I get this error sometimes
Author
Owner

@Abhishek-M-K commented on GitHub (Nov 8, 2025):

having the same issue with better auth package version 1.3.25

my frontend and backend both are built in NextJs. I don't get any problem in localhost but in production I see in the logs I get this error sometimes

Same issue over here with better auth ver: 1.3.27

<!-- gh-comment-id:3506516688 --> @Abhishek-M-K commented on GitHub (Nov 8, 2025): > having the same issue with better auth package version 1.3.25 > > my frontend and backend both are built in NextJs. I don't get any problem in localhost but in production I see in the logs I get this error sometimes Same issue over here with better auth ver: 1.3.27
Author
Owner

@LeulAria commented on GitHub (Nov 9, 2025):

++ on this ?error=state_mismatch why is it closed ?

<!-- gh-comment-id:3508878618 --> @LeulAria commented on GitHub (Nov 9, 2025): ++ on this ?error=state_mismatch why is it closed ?
Author
Owner

@maxktz commented on GitHub (Nov 9, 2025):

downgrading from 1.3.27 to 1.3.13 worked for me, not sure why

Also downgraded and worked!

<!-- gh-comment-id:3508895715 --> @maxktz commented on GitHub (Nov 9, 2025): > downgrading from 1.3.27 to 1.3.13 worked for me, not sure why Also downgraded and worked!
Author
Owner

@Ash200042123 commented on GitHub (Nov 10, 2025):

I've dowgraded to 1.3.13 but still getting the error

<!-- gh-comment-id:3508994130 --> @Ash200042123 commented on GitHub (Nov 10, 2025): I've dowgraded to 1.3.13 but still getting the error
Author
Owner

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

Getting the same error with Fastify and Nextjs, any solution?

<!-- gh-comment-id:3522337065 --> @felipinas commented on GitHub (Nov 12, 2025): Getting the same error with Fastify and Nextjs, any solution?
Author
Owner

@fdemir commented on GitHub (Nov 12, 2025):

Getting the same error with Fastify and Nextjs, any solution?

Make sure you have the same version between api/web and correct credentials.

<!-- gh-comment-id:3523044218 --> @fdemir commented on GitHub (Nov 12, 2025): > Getting the same error with Fastify and Nextjs, any solution? Make sure you have the same version between api/web and correct credentials.
Author
Owner

@van-sprundel commented on GitHub (Nov 12, 2025):

Can confirm this has nothing to do with expo, i was using this with express and after downgrading it worked again

"better-auth": "1.3.13"

Clearly broken, this issue shouldn't be closed

<!-- gh-comment-id:3524120017 --> @van-sprundel commented on GitHub (Nov 12, 2025): Can confirm this has nothing to do with expo, i was using this with express and after downgrading it worked again ```json "better-auth": "1.3.13" ``` Clearly broken, this issue shouldn't be closed
Author
Owner

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

I have downgraded to 1.3.13 in my NextJs webapp but still getting the error. can anyone help?
2025-11-14T02:14:39.476Z ERROR [Better Auth]: State Mismatch. Verification not found

<!-- gh-comment-id:3530799730 --> @Ash200042123 commented on GitHub (Nov 14, 2025): I have downgraded to 1.3.13 in my NextJs webapp but still getting the error. can anyone help? 2025-11-14T02:14:39.476Z ERROR [Better Auth]: State Mismatch. Verification not found
Author
Owner

@van-sprundel commented on GitHub (Nov 14, 2025):

Make sure that in your lock file it matches the version and you don't have a ^ at the start, anything above will fail

<!-- gh-comment-id:3531820934 --> @van-sprundel commented on GitHub (Nov 14, 2025): Make sure that in your lock file it matches the version and you don't have a `^` at the start, anything above will fail
Author
Owner

@DanielBoxer commented on GitHub (Nov 16, 2025):

I also have this error with a client and server on different domains and using social google login. But I use token auth instead of cookies.

<!-- gh-comment-id:3537297033 --> @DanielBoxer commented on GitHub (Nov 16, 2025): I also have this error with a client and server on different domains and using social google login. But I use token auth instead of cookies.
Author
Owner

@agustinmorantes commented on GitHub (Nov 16, 2025):

I can also confirm this is still happening in the latest version, and downgrading to 1.3.13 works in expressjs.

<!-- gh-comment-id:3539315328 --> @agustinmorantes commented on GitHub (Nov 16, 2025): I can also confirm this is still happening in the latest version, and downgrading to 1.3.13 works in expressjs.
Author
Owner

@van-sprundel commented on GitHub (Nov 18, 2025):

Was trying to debug what might be the breaking change and I feel like it was due to changes in src/oauth2/state.ts. This was done for security reasons but basically broke OAuth flow. The oauth-proxy plugin already sets skipStateCookieCheck = true (see packages/better-auth/src/plugins/oauth-proxy/index.ts) to work around this exact issue.

Rn it defaults to "database" strategy, which requires BOTH a state in DB and cookie/browser. This generally breaks in OAuth (although I can't figure out why)

<!-- gh-comment-id:3547668221 --> @van-sprundel commented on GitHub (Nov 18, 2025): Was trying to debug what might be the breaking change and I feel like it was due to changes in `src/oauth2/state.ts`. This was done for security reasons but basically broke OAuth flow. The oauth-proxy plugin already sets skipStateCookieCheck = true (see `packages/better-auth/src/plugins/oauth-proxy/index.ts`) to work around this exact issue. Rn it defaults to "database" strategy, which requires BOTH a state in DB and cookie/browser. This generally breaks in OAuth (although I can't figure out why)
Author
Owner

@van-sprundel commented on GitHub (Nov 24, 2025):

Idk who to tag for this but there's 2 things we could do

  1. remove cookie requirement like it was pre-v1.4.0 (but it's good to keep the double verification)
  2. change the default in create-context from "database" to "cookie" to match the JSDoc documentation (this is also standard oauth behavior and i would prefer this imo).

I've created a PR for 2, lmk what the team thinks
https://github.com/better-auth/better-auth/pull/6278

<!-- gh-comment-id:3572870295 --> @van-sprundel commented on GitHub (Nov 24, 2025): Idk who to tag for this but there's 2 things we could do 1. remove cookie requirement like it was pre-v1.4.0 (but it's good to keep the double verification) 2. change the default in create-context from "database" to "cookie" to match the JSDoc documentation (this is also standard oauth behavior and i would prefer this imo). I've created a PR for 2, lmk what the team thinks https://github.com/better-auth/better-auth/pull/6278
Author
Owner

@tjx666 commented on GitHub (Dec 5, 2025):

Same here but for auth0

Image
<!-- gh-comment-id:3615172134 --> @tjx666 commented on GitHub (Dec 5, 2025): Same here but for auth0 <img width="1440" height="854" alt="Image" src="https://github.com/user-attachments/assets/9d5beff7-f2b6-470f-bfaa-971383e83cb1" />
Author
Owner

@isener commented on GitHub (Feb 1, 2026):

Thanks for raising this issue here;

It also worked for me once I've upgraded both @better-auth/expo and better-auth packages from ^1.3.34 to ^1.4.18, FYI

<!-- gh-comment-id:3832293422 --> @isener commented on GitHub (Feb 1, 2026): Thanks for raising this issue here; It also worked for me once I've upgraded both **@better-auth/expo** and **better-auth** packages from ^1.3.34 to ^1.4.18, FYI
Author
Owner

@github-actions[bot] commented on GitHub (Apr 1, 2026):

This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.

<!-- gh-comment-id:4166561620 --> @github-actions[bot] commented on GitHub (Apr 1, 2026): This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10147