[GH-ISSUE #6847] Expo + stateless (v1.4): OAuth callback fails with State Mismatch and redirects to error=please_restart_the_process (Keycloak/genericOAuth) #27975

Closed
opened 2026-04-17 19:17:02 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @ruff-exec on GitHub (Dec 18, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6847

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create an Expo app using Expo Router API routes.
  2. Configure Better Auth v1.4+ with:
    • storeStateStrategy: "cookie"
    • Stateless sessions via session.cookieCache
    • expo() plugin
    • genericOAuth with Keycloak
  3. Configure the Expo client with:
    • createAuthClient
    • expoClient(...)
    • genericOAuthClient()
  4. Run the app on Android (Expo / Zebra device) and trigger login via:
    authClient.signIn.oauth2({
               providerId: 'keycloak',
               callbackURL: '/',
             })
    

Note:
The same setup works correctly when accessing the app via the browser (web), but fails only when login is initiated from the native app.

Current vs. Expected behavior

Current Behavior

After returning from Keycloak, the OAuth callback fails with:

ERROR [Better Auth]: State Mismatch. OAuth state cookie not found

and redirects to:

/api/auth/error?error=please_restart_the_process

This happens even though the OAuth state cookie is present on the callback request and the state query parameter matches the cookie prefix.

Observed behavior from logs

During a single native login attempt, the following occurs:

  1. POST /api/auth/sign-in/oauth2

    • Cookie present:
      better-auth.oauth_state=...
      
    • Server sets:
      Set-Cookie: better-auth.oauth_state=...
      
  2. GET /api/auth/expo-authorization-proxy

    • No cookies (expected)
    • Server sets:
      Set-Cookie: better-auth.state=<state>.<signature>
      
  3. GET /api/auth/oauth2/callback/keycloak?state=<state>

    • Cookie present:
      better-auth.state=<state>.<signature>
      
    • Server logs:
      State Mismatch. OAuth state cookie not found
      

This shows that two different cookie names are used during the same OAuth flow:

  • better-auth.oauth_state (cookie-based state strategy)
  • better-auth.state (default/db-style state strategy)

Because of this, the callback validation fails and triggers please_restart_the_process.


Expected Behavior

When storeStateStrategy: "cookie" is configured:

  • The OAuth state should be stored consistently under one cookie name for the entire flow.
  • The cookie set before redirect should be the same cookie read during the callback.
  • The callback should succeed when:
    • the state query parameter matches the cookie prefix
    • the cookie is present and valid

The native (Expo) OAuth flow should behave the same as the web flow.

What version of Better Auth are you using?

1.4.7

System info

{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025",
    "release": "6.6.87.2-microsoft-standard-WSL2",
    "cpuCount": 8,
    "cpuModel": "11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz",
    "totalMemory": "11.68 GB",
    "freeMemory": "6.78 GB"
  },
  "node": {
    "version": "v22.12.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.7.0"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "19.1.4"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "1.4.7",
    "config": {
      "plugins": [
        {
          "name": "expo",
          "config": {
            "id": "expo",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {}
          }
        },
        {
          "name": "generic-oauth",
          "config": {
            "id": "generic-oauth",
            "endpoints": {},
            "$ERROR_CODES": {
              "INVALID_OAUTH_CONFIGURATION": "Invalid OAuth configuration",
              "TOKEN_URL_NOT_FOUND": "Invalid OAuth configuration. Token URL not found.",
              "PROVIDER_CONFIG_NOT_FOUND": "No config found for provider",
              "PROVIDER_ID_REQUIRED": "Provider ID is required",
              "INVALID_OAUTH_CONFIG": "Invalid OAuth configuration.",
              "SESSION_REQUIRED": "Session is required"
            }
          }
        }
      ],
      "trustedOrigins": [
        "com.company.app://"
      ],
      "session": {
        "cookieCache": {
          "enabled": true,
          "maxAge": 7200,
          "strategy": "jwt",
          "refreshCache": true
        }
      },
      "account": {
        "storeStateStrategy": "cookie",
        "storeAccountCookie": true,
        "skipStateCookieCheck": true
      }
    }
  }
}

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

Backend, Client

Auth config (if applicable)

import 'server-only'
import { betterAuth } from 'better-auth'
import { genericOAuth, keycloak } from 'better-auth/plugins'
import { expo } from '@better-auth/expo'

export const auth = betterAuth({
  plugins: [
    expo(),
    genericOAuth({
      config: [
        keycloak({
          clientId: process.env.KEYCLOAK_CLIENT_ID!,
          clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
          issuer: process.env.KEYCLOAK_ISSUER!,
          scopes: ['openid', 'profile', 'email'],
          pkce: true,
        }),
      ],
    }),
  ],
  trustedOrigins: [
    'com.company.app://',
    // Development mode - Expo's exp:// scheme with local IP ranges
    ...(process.env.NODE_ENV === 'development'
      ? [
          'exp://*/*', // Trust all Expo development URLs
          'exp://10.0.0.*:*/*', // Trust 10.0.0.x IP range
          'exp://192.168.*.*:*/*', // Trust 192.168.x.x IP range
          'exp://172.*.*.*:*/*', // Trust 172.x.x.x IP range
          'exp://localhost:*/*', // Trust localhost
        ]
      : []),
  ],
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 60 * 60 * 2, // 2 hours
      strategy: 'jwt',
      refreshCache: true,
    },
  },
  account: {
    storeStateStrategy: 'cookie',
    storeAccountCookie: true,
    skipStateCookieCheck: true,
  },
})

Additional context

  • This issue occurs only in the native Expo flow using:
    • expo() plugin
    • /api/auth/expo-authorization-proxy
  • The same configuration works correctly when logging in via the browser (web).
  • Logs suggest that:
    • the Expo authorization proxy sets a better-auth.state cookie
    • while the app-initiated OAuth flow uses better-auth.oauth_state
  • This appears related to the new stateless / cookie-based state handling introduced in v1.4.
  • Secondary storage and skipStateCookieCheck do not resolve the issue.
  • The error always ends in:
    /api/auth/error?error=please_restart_the_process
    

Debug instrumentation used

To verify cookies and Set-Cookie behavior in the Expo Router API route, I temporarily replaced the handler export with this logging wrapper:

// app/api/auth/[...auth]+api.ts
import { auth } from "@/server/auth";

// Keep logs readable and avoid leaking unrelated cookies.
function extractBetterAuthCookies(cookieHeader: string | null) {
  if (!cookieHeader) return {};
  const out: Record<string, string> = {};
  for (const part of cookieHeader.split(/;\s*/)) {
    const i = part.indexOf("=");
    if (i === -1) continue;
    const name = part.slice(0, i).trim();
    const value = part.slice(i + 1);
    if (name.startsWith("better-auth.")) out[name] = value;
  }
  return out;
}

async function handle(req: Request) {
  const url = new URL(req.url);
  const cookie = req.headers.get("cookie");

  console.log("=== BetterAuth API Route ===");
  console.log("Method:", req.method);
  console.log("Request URL:", url.toString());
  console.log("Cookie header:", cookie);
  console.log("BetterAuth cookies:", extractBetterAuthCookies(cookie));

  const res = await auth.handler(req);

  const setCookie = res.headers.get("set-cookie");
  if (setCookie) {
    console.log("Set-Cookie header:", setCookie);

    const names = setCookie
      .split(/,(?=\\s*[^;]+=)/)
      .map((c) => c.split(";", 1)[0])
      .map((kv) => kv.split("=", 1)[0]?.trim())
      .filter(Boolean);
    console.log("Set-Cookie names:", names);
  } else {
    console.log("Set-Cookie header: <none>");
  }

  console.log("============================");

  return res;
}

export async function GET(req: Request) {
  return handle(req);
}

export async function POST(req: Request) {
  return handle(req);
}

Full logs

Full logs

Click to expand full logs
Android Bundled 411ms node_modules/expo-network/build/Network.js (613 modules)
RSC(Web) Bundled 960ms src/app/api/auth/[...auth]+api.ts (640 modules)
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/get-session
Cookie header: ; better-auth.oauth_state=f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c
BetterAuth cookies: {
  'better-auth.oauth_state': 'f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: POST
Request URL: http://localhost:8081/api/auth/sign-in/oauth2
Cookie header: ; better-auth.oauth_state=f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c
BetterAuth cookies: {
  'better-auth.oauth_state': 'f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c'
}
Set-Cookie header: better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663; Max-Age=600000; Path=/; HttpOnly; SameSite=Lax
Set-Cookie names: [ 'better-auth.oauth_state' ]
============================
Android Bundled 468ms node_modules/expo-web-browser/build/WebBrowser.js (613 modules)
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/get-session
Cookie header: ; better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663
BetterAuth cookies: {
  'better-auth.oauth_state': 'd625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/expo-authorization-proxy?authorizationURL=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse%2Fprotocol%2Fopenid-connect%2Fauth%3Fresponse_type%3Dcode%26client_id%3Dmobile-ilogistics-expo%26state%3Dju2A8oxZi3EPQt9WPdutU38OX5lhS7kk%26scope%3Dopenid%2Bprofile%2Bemail%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8081%252Fapi%252Fauth%252Foauth2%252Fcallback%252Fkeycloak%26code_challenge_method%3DS256%26code_challenge%3D6WAEqFMVHrYeO5zLlrRTRifu7DRNV0wvT8uhT-gnvbg
Cookie header: null
BetterAuth cookies: {}
Set-Cookie header: better-auth.state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D; Max-Age=300000; Path=/; HttpOnly; SameSite=Lax
Set-Cookie names: [ 'better-auth.state' ]
============================
=== BetterAuth API Route ===
2025-12-18T07:05:51.908Z ERROR [Better Auth]: State Mismatch. OAuth state cookie not found { state: 'ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk' }
Method: GET
Request URL: http://localhost:8081/api/auth/oauth2/callback/keycloak?state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk&session_state=686a7366-f787-48ef-8a5b-d75fd1224437&iss=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse&code=0f603b1a-3677-4f00-9181-37c03e5e3066.686a7366-f787-48ef-8a5b-d75fd1224437.4c9fcf16-e1e3-4936-a511-298d877cd55f
Cookie header: better-auth.state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D
BetterAuth cookies: {
  'better-auth.state': 'ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/error?error=please_restart_the_process
Cookie header: better-auth.state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D
BetterAuth cookies: {
  'better-auth.state': 'ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/get-session
Cookie header: ; better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663
BetterAuth cookies: {
  'better-auth.oauth_state': 'd625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/get-session
Cookie header: ; better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663
BetterAuth cookies: {
  'better-auth.oauth_state': 'd625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663'
}
Set-Cookie header: <none>
============================
› Stopped server
Originally created by @ruff-exec on GitHub (Dec 18, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6847 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create an Expo app using **Expo Router API routes**. 2. Configure Better Auth **v1.4+** with: - `storeStateStrategy: "cookie"` - Stateless sessions via `session.cookieCache` - `expo()` plugin - `genericOAuth` with **Keycloak** 3. Configure the Expo client with: - `createAuthClient` - `expoClient(...)` - `genericOAuthClient()` 4. Run the app on **Android (Expo / Zebra device)** and trigger login via: ```ts authClient.signIn.oauth2({ providerId: 'keycloak', callbackURL: '/', }) ``` > Note: > The same setup works correctly when accessing the app **via the browser (web)**, but fails **only when login is initiated from the native app**. ### Current vs. Expected behavior ## Current Behavior After returning from Keycloak, the OAuth callback fails with: ``` ERROR [Better Auth]: State Mismatch. OAuth state cookie not found ``` and redirects to: ``` /api/auth/error?error=please_restart_the_process ``` This happens **even though the OAuth `state` cookie is present on the callback request** and the `state` query parameter matches the cookie prefix. ### Observed behavior from logs During a single native login attempt, the following occurs: 1. **POST** `/api/auth/sign-in/oauth2` - Cookie present: ``` better-auth.oauth_state=... ``` - Server sets: ``` Set-Cookie: better-auth.oauth_state=... ``` 2. **GET** `/api/auth/expo-authorization-proxy` - No cookies (expected) - Server sets: ``` Set-Cookie: better-auth.state=<state>.<signature> ``` 3. **GET** `/api/auth/oauth2/callback/keycloak?state=<state>` - Cookie present: ``` better-auth.state=<state>.<signature> ``` - Server logs: ``` State Mismatch. OAuth state cookie not found ``` This shows that **two different cookie names are used during the same OAuth flow**: - `better-auth.oauth_state` (cookie-based state strategy) - `better-auth.state` (default/db-style state strategy) Because of this, the callback validation fails and triggers `please_restart_the_process`. --- ## Expected Behavior When `storeStateStrategy: "cookie"` is configured: - The OAuth state should be stored **consistently under one cookie name** for the entire flow. - The cookie set before redirect should be the same cookie read during the callback. - The callback should succeed when: - the `state` query parameter matches the cookie prefix - the cookie is present and valid The native (Expo) OAuth flow should behave the same as the web flow. ### What version of Better Auth are you using? 1.4.7 ### System info ```bash { "system": { "platform": "linux", "arch": "x64", "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", "release": "6.6.87.2-microsoft-standard-WSL2", "cpuCount": 8, "cpuModel": "11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz", "totalMemory": "11.68 GB", "freeMemory": "6.78 GB" }, "node": { "version": "v22.12.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.7.0" }, "frameworks": [ { "name": "react", "version": "19.1.4" } ], "databases": null, "betterAuth": { "version": "1.4.7", "config": { "plugins": [ { "name": "expo", "config": { "id": "expo", "hooks": { "after": [ {} ] }, "endpoints": {} } }, { "name": "generic-oauth", "config": { "id": "generic-oauth", "endpoints": {}, "$ERROR_CODES": { "INVALID_OAUTH_CONFIGURATION": "Invalid OAuth configuration", "TOKEN_URL_NOT_FOUND": "Invalid OAuth configuration. Token URL not found.", "PROVIDER_CONFIG_NOT_FOUND": "No config found for provider", "PROVIDER_ID_REQUIRED": "Provider ID is required", "INVALID_OAUTH_CONFIG": "Invalid OAuth configuration.", "SESSION_REQUIRED": "Session is required" } } } ], "trustedOrigins": [ "com.company.app://" ], "session": { "cookieCache": { "enabled": true, "maxAge": 7200, "strategy": "jwt", "refreshCache": true } }, "account": { "storeStateStrategy": "cookie", "storeAccountCookie": true, "skipStateCookieCheck": true } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import 'server-only' import { betterAuth } from 'better-auth' import { genericOAuth, keycloak } from 'better-auth/plugins' import { expo } from '@better-auth/expo' export const auth = betterAuth({ plugins: [ expo(), genericOAuth({ config: [ keycloak({ clientId: process.env.KEYCLOAK_CLIENT_ID!, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, issuer: process.env.KEYCLOAK_ISSUER!, scopes: ['openid', 'profile', 'email'], pkce: true, }), ], }), ], trustedOrigins: [ 'com.company.app://', // Development mode - Expo's exp:// scheme with local IP ranges ...(process.env.NODE_ENV === 'development' ? [ 'exp://*/*', // Trust all Expo development URLs 'exp://10.0.0.*:*/*', // Trust 10.0.0.x IP range 'exp://192.168.*.*:*/*', // Trust 192.168.x.x IP range 'exp://172.*.*.*:*/*', // Trust 172.x.x.x IP range 'exp://localhost:*/*', // Trust localhost ] : []), ], session: { cookieCache: { enabled: true, maxAge: 60 * 60 * 2, // 2 hours strategy: 'jwt', refreshCache: true, }, }, account: { storeStateStrategy: 'cookie', storeAccountCookie: true, skipStateCookieCheck: true, }, }) ``` ### Additional context - This issue occurs **only in the native Expo flow** using: - `expo()` plugin - `/api/auth/expo-authorization-proxy` - The same configuration works correctly when logging in via the browser (web). - Logs suggest that: - the Expo authorization proxy sets a `better-auth.state` cookie - while the app-initiated OAuth flow uses `better-auth.oauth_state` - This appears related to the **new stateless / cookie-based state handling introduced in v1.4**. - Secondary storage and `skipStateCookieCheck` do not resolve the issue. - The error always ends in: ``` /api/auth/error?error=please_restart_the_process ``` ## Debug instrumentation used To verify cookies and `Set-Cookie` behavior in the Expo Router API route, I temporarily replaced the handler export with this logging wrapper: ```ts // app/api/auth/[...auth]+api.ts import { auth } from "@/server/auth"; // Keep logs readable and avoid leaking unrelated cookies. function extractBetterAuthCookies(cookieHeader: string | null) { if (!cookieHeader) return {}; const out: Record<string, string> = {}; for (const part of cookieHeader.split(/;\s*/)) { const i = part.indexOf("="); if (i === -1) continue; const name = part.slice(0, i).trim(); const value = part.slice(i + 1); if (name.startsWith("better-auth.")) out[name] = value; } return out; } async function handle(req: Request) { const url = new URL(req.url); const cookie = req.headers.get("cookie"); console.log("=== BetterAuth API Route ==="); console.log("Method:", req.method); console.log("Request URL:", url.toString()); console.log("Cookie header:", cookie); console.log("BetterAuth cookies:", extractBetterAuthCookies(cookie)); const res = await auth.handler(req); const setCookie = res.headers.get("set-cookie"); if (setCookie) { console.log("Set-Cookie header:", setCookie); const names = setCookie .split(/,(?=\\s*[^;]+=)/) .map((c) => c.split(";", 1)[0]) .map((kv) => kv.split("=", 1)[0]?.trim()) .filter(Boolean); console.log("Set-Cookie names:", names); } else { console.log("Set-Cookie header: <none>"); } console.log("============================"); return res; } export async function GET(req: Request) { return handle(req); } export async function POST(req: Request) { return handle(req); } ``` ## Full logs ### Full logs <details> <summary>Click to expand full logs</summary> ```txt Android Bundled 411ms node_modules/expo-network/build/Network.js (613 modules) RSC(Web) Bundled 960ms src/app/api/auth/[...auth]+api.ts (640 modules) === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/get-session Cookie header: ; better-auth.oauth_state=f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c BetterAuth cookies: { 'better-auth.oauth_state': 'f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: POST Request URL: http://localhost:8081/api/auth/sign-in/oauth2 Cookie header: ; better-auth.oauth_state=f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c BetterAuth cookies: { 'better-auth.oauth_state': 'f0918ac00f4ca8d06e66e6e92324c36dd65095737e18c2e0c8a86ad9e1603c6cce1343865ffeb52e65803d1f160ad9a7c9d00cdf9ba1da3c5598a6dfc66ed3fc8538cff767363f2c803703a5e937b649f3575a461f21e7fb761dad9c9df64e192cd0f1687f7b72dfd033f28daa7af72712e72876e50fb17a86b15bc8a2b81d81c451e33175b26bd7ad0ba81323e52c7fad1679796f57a392eb899bf2b3ce519500de76ed2d09d6ea9979c9ba85472536754fde415a4c586c05d937b01d8593cc2335ba88fd9f287f062b249dc604a53bf3ea8559bc6dccb4e695d30f657188a1a125fc54d3f3e678041a417ffbc40397911e1626f4441e1d1cfb13170b9ce4b6d45e1f084142c58c8eab13eae0a6d86254f77d8d64eeb4b28f04960dbf72b91c0859afaa0b27975e2de82408e0fff64c' } Set-Cookie header: better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663; Max-Age=600000; Path=/; HttpOnly; SameSite=Lax Set-Cookie names: [ 'better-auth.oauth_state' ] ============================ Android Bundled 468ms node_modules/expo-web-browser/build/WebBrowser.js (613 modules) === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/get-session Cookie header: ; better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663 BetterAuth cookies: { 'better-auth.oauth_state': 'd625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/expo-authorization-proxy?authorizationURL=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse%2Fprotocol%2Fopenid-connect%2Fauth%3Fresponse_type%3Dcode%26client_id%3Dmobile-ilogistics-expo%26state%3Dju2A8oxZi3EPQt9WPdutU38OX5lhS7kk%26scope%3Dopenid%2Bprofile%2Bemail%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8081%252Fapi%252Fauth%252Foauth2%252Fcallback%252Fkeycloak%26code_challenge_method%3DS256%26code_challenge%3D6WAEqFMVHrYeO5zLlrRTRifu7DRNV0wvT8uhT-gnvbg Cookie header: null BetterAuth cookies: {} Set-Cookie header: better-auth.state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D; Max-Age=300000; Path=/; HttpOnly; SameSite=Lax Set-Cookie names: [ 'better-auth.state' ] ============================ === BetterAuth API Route === 2025-12-18T07:05:51.908Z ERROR [Better Auth]: State Mismatch. OAuth state cookie not found { state: 'ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk' } Method: GET Request URL: http://localhost:8081/api/auth/oauth2/callback/keycloak?state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk&session_state=686a7366-f787-48ef-8a5b-d75fd1224437&iss=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse&code=0f603b1a-3677-4f00-9181-37c03e5e3066.686a7366-f787-48ef-8a5b-d75fd1224437.4c9fcf16-e1e3-4936-a511-298d877cd55f Cookie header: better-auth.state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D BetterAuth cookies: { 'better-auth.state': 'ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/error?error=please_restart_the_process Cookie header: better-auth.state=ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D BetterAuth cookies: { 'better-auth.state': 'ju2A8oxZi3EPQt9WPdutU38OX5lhS7kk.xggsHckhUqZo%2B%2B6jX9lBembbg%2FNuVlQxZSp3FaZViso%3D' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/get-session Cookie header: ; better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663 BetterAuth cookies: { 'better-auth.oauth_state': 'd625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/get-session Cookie header: ; better-auth.oauth_state=d625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663 BetterAuth cookies: { 'better-auth.oauth_state': 'd625a78b896552eaf9045e63cc4a0dc7a248025bd1adc77e872e3cfabc919f958435c3a1aefc11481cb1bf96cef0cae15851362576356c4f695b35c218bf9485dc2ea712b9f7f2ce4328ff8ca012fe3d720cecb0031df0644dbdc786fdd4c53131cb926c11f6b58362c87ebcd72737ba81792c3a2e2874d81138cff04f8ae2df7bdd2be7b95f98411dfa996ac7cc7f886ec080fbe8811bdb24e8d3967bba63d4a9dc4c07cffec72e2d12f598c8ada6fb6fc75b2bf8dfb6d58c724f839c644b4d9ffd6e0a0e613235b2ea12dda2ce607103a0a5310940990f7ca25a6b3a75291da01e8b6857463b4718e1493685d6772c3fd8d3d06c5fa02db963540ad7e1ffa42879ca6ebd943d5be4a9c910cf031c375069ebea30b9c6b765287efa7945b24a06029c49a70bd0c3b38a926adc3b1663' } Set-Cookie header: <none> ============================ › Stopped server ``` </details>
GiteaMirror added the lockedbug labels 2026-04-17 19:17:02 -05:00
Author
Owner

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

tag @better-auth-agent if you would like an answer from the Agent

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3668810813 --> @better-auth-agent[bot] commented on GitHub (Dec 18, 2025): tag @better-auth-agent if you would like an answer from the Agent _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@dosubot[bot] commented on GitHub (Dec 18, 2025):

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

This is a known issue in Better Auth v1.4+ with Expo and stateless sessions: the Expo authorization proxy sets a cookie named better-auth.state, but when you use storeStateStrategy: "cookie", the OAuth callback expects a cookie named better-auth.oauth_state. This mismatch causes the "State Mismatch. OAuth state cookie not found" error in native flows, even though the state value is present and matches. The web flow works because it doesn't use the Expo proxy, so the cookie names stay consistent.

This is not a misconfiguration on your part—it's a bug or oversight in the Expo plugin's proxy endpoint, which doesn't honor the storeStateStrategy: "cookie" setting when setting the state cookie during the proxy step. The callback handler then fails to find the expected cookie and aborts the flow. This behavior is confirmed in the code and discussed in multiple issues and PRs (source, source, source).

Recent work on the oauth-proxy plugin (see PR #6131) added stateless session support by encrypting state and cookies into the provider URL and restoring them on callback, but if you're still seeing this mismatch, the fix may not be fully released or integrated in your version.

Workarounds and tips:

  • Make sure both better-auth and @better-auth/expo are on the latest versions.
  • Use the Expo client's built-in OAuth flow (authClient.signIn.social), not a manual POST + WebBrowser workaround (source).
  • Double-check that your trustedOrigins and cookie attributes (SameSite=None, Secure=true) are set for cross-origin support (source).
  • If you need an immediate fix, consider temporarily switching to the default (database) state strategy, or patching the Expo proxy endpoint to set the correct cookie name (better-auth.oauth_state) in stateless mode.

This is a real bug, not a config error. Keep an eye on the repo for updates or consider contributing a patch if you need a fast resolution.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3668821290 --> @dosubot[bot] commented on GitHub (Dec 18, 2025): <!-- Greeting --> Hi @ruff-exec! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known issue in Better Auth v1.4+ with Expo and stateless sessions: the Expo authorization proxy sets a cookie named `better-auth.state`, but when you use `storeStateStrategy: "cookie"`, the OAuth callback expects a cookie named `better-auth.oauth_state`. This mismatch causes the "State Mismatch. OAuth state cookie not found" error in native flows, even though the state value is present and matches. The web flow works because it doesn't use the Expo proxy, so the cookie names stay consistent. This is not a misconfiguration on your part—it's a bug or oversight in the Expo plugin's proxy endpoint, which doesn't honor the `storeStateStrategy: "cookie"` setting when setting the state cookie during the proxy step. The callback handler then fails to find the expected cookie and aborts the flow. This behavior is confirmed in the code and discussed in multiple issues and PRs ([source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/expo/src/routes.ts#L5-L34), [source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/oauth2/state.ts#L49-L210), [source](https://github.com/better-auth/better-auth/issues/6207)). Recent work on the oauth-proxy plugin (see [PR #6131](https://github.com/better-auth/better-auth/pull/6131)) added stateless session support by encrypting state and cookies into the provider URL and restoring them on callback, but if you're still seeing this mismatch, the fix may not be fully released or integrated in your version. **Workarounds and tips:** - Make sure both `better-auth` and `@better-auth/expo` are on the latest versions. - Use the Expo client's built-in OAuth flow (`authClient.signIn.social`), not a manual POST + WebBrowser workaround ([source](https://github.com/better-auth/better-auth/issues/6207)). - Double-check that your `trustedOrigins` and cookie attributes (`SameSite=None`, `Secure=true`) are set for cross-origin support ([source](https://github.com/better-auth/better-auth/issues/6483)). - If you need an immediate fix, consider temporarily switching to the default (database) state strategy, or patching the Expo proxy endpoint to set the correct cookie name (`better-auth.oauth_state`) in stateless mode. This is a real bug, not a config error. Keep an eye on the repo for updates or consider contributing a patch if you need a fast resolution. <!-- 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/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ed69c333-f080-44e8-84e9-c73196c82023?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20a%20known%20issue%20in%20Better%20Auth%20v1.4%2B%20with%20Expo%20and%20stateless%20sessions%3A%20the%20Expo%20authorization%20proxy%20sets%20a%20cookie%20named%20%60better-auth.state%60%2C%20but%20when%20you%20use%20%60storeStateStrategy%3A%20%22cookie%22%60%2C%20the%20OAuth%20callback%20expects%20a%20cookie%20named%20%60better-auth.oauth_state%60.%20This%20mismatch%20causes%20the%20%22State%20Mismatch.%20OAuth%20state%20cookie%20not%20found%22%20error%20in%20native%20flows%2C%20even%20though%20the%20state%20value%20is%20present%20and%20matches.%20The%20web%20flow%20works%20because%20it%20doesn%27t%20use%20the%20Expo%20proxy%2C%20so%20the%20cookie%20names%20stay%20consistent.%0A%0AThis%20is%20not%20a%20misconfiguration%20on%20your%20part%E2%80%94it%27s%20a%20bug%20or%20oversight%20in%20the%20Expo%20plugin%27s%20proxy%20endpoint%2C%20which%20doesn%27t%20honor%20the%20%60storeStateStrategy%3A%20%22cookie%22%60%20setting%20when%20setting%20the%20state%20cookie%20during%20the%20proxy%20step.%20The%20callback%20handler%20then%20fails%20to%20find%20the%20expected%20cookie%20and%20aborts%20the%20flow.%20This%20behavior%20is%20confirmed%20in%20the%20code%20and%20discussed%20in%20multiple%20issues%20and%20PRs%20%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/expo/src/routes.ts%23L5-L34%29%2C%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/oauth2/state.ts%23L49-L210%29%2C%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/6207%29%29.%0A%0ARecent%20work%20on%20the%20oauth-proxy%20plugin%20%28see%20%5BPR%20%236131%5D%28https%3A//github.com/better-auth/better-auth/pull/6131%29%29%20added%20stateless%20session%20support%20by%20encrypting%20state%20and%20cookies%20into%20the%20provider%20URL%20and%20restoring%20them%20on%20callback%2C%20but%20if%20you%27re%20still%20seeing%20this%20mismatch%2C%20the%20fix%20may%20not%20be%20fully%20released%20or%20integrated%20in%20your%20version.%0A%0A%2A%2AWorkarounds%20and%20tips%3A%2A%2A%0A-%20Make%20sure%20both%20%60better-auth%60%20and%20%60%40better-auth/expo%60%20are%20on%20the%20latest%20versions.%0A-%20Use%20the%20Expo%20client%27s%20built-in%20OAuth%20flow%20%28%60authClient.signIn.social%60%29%2C%20not%20a%20manual%20POST%20%2B%20WebBrowser%20workaround%20%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/6207%29%29.%0A-%20Double-check%20that%20your%20%60trustedOrigins%60%20and%20cookie%20attributes%20%28%60SameSite%3DNone%60%2C%20%60Secure%3Dtrue%60%29%20are%20set%20for%20cross-origin%20support%20%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/issues/6483%29%29.%0A-%20If%20you%20need%20an%20immediate%20fix%2C%20consider%20temporarily%20switching%20to%20the%20default%20%28database%29%20state%20strategy%2C%20or%20patching%20the%20Expo%20proxy%20endpoint%20to%20set%20the%20correct%20cookie%20name%20%28%60better-auth.oauth_state%60%29%20in%20stateless%20mode.%0A%0AThis%20is%20a%20real%20bug%2C%20not%20a%20config%20error.%20Keep%20an%20eye%20on%20the%20repo%20for%20updates%20or%20consider%20contributing%20a%20patch%20if%20you%20need%20a%20fast%20resolution.)&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/6847)
Author
Owner

@ruff-exec commented on GitHub (Dec 18, 2025):

@dosu I am using the latest version, I am also using the client's built-in OAuth flow, trustedOrigins should also be ok.

I tried to fix it with the last workaround you mentioned.

I patched the Expo Router API route so that for the endpoint:

/api/auth/expo-authorization-proxy

the response header

Set-Cookie: better-auth.state=...

is rewritten to

Set-Cookie: better-auth.oauth_state=...

This resolves the cookie name mismatch, but the OAuth callback then fails with:

ERROR [Better Auth]: Failed to decrypt or parse OAuth state cookie
Error: hex string expected, got unpadded hex of length …

This demonstrates that better-auth.state and better-auth.oauth_state are not
interchangeable and do not just differ by name:

  • better-auth.state contains a signed state string (default / database strategy)
  • better-auth.oauth_state expects an encrypted stateData payload (cookie strategy)

Because of this, simply renaming the cookie does not work.

<!-- gh-comment-id:3669223498 --> @ruff-exec commented on GitHub (Dec 18, 2025): @dosu I am using the latest version, I am also using the client's built-in OAuth flow, trustedOrigins should also be ok. I tried to fix it with the last workaround you mentioned. I patched the Expo Router API route so that for the endpoint: `/api/auth/expo-authorization-proxy` the response header `Set-Cookie: better-auth.state=...` is rewritten to `Set-Cookie: better-auth.oauth_state=...` This resolves the cookie *name* mismatch, but the OAuth callback then fails with: ``` ERROR [Better Auth]: Failed to decrypt or parse OAuth state cookie Error: hex string expected, got unpadded hex of length … ``` This demonstrates that `better-auth.state` and `better-auth.oauth_state` are not interchangeable and do not just differ by name: - `better-auth.state` contains a signed state string (default / database strategy) - `better-auth.oauth_state` expects an encrypted `stateData` payload (cookie strategy) Because of this, simply renaming the cookie does not work.
Author
Owner

@dosubot[bot] commented on GitHub (Dec 18, 2025):

You're right—just renaming the cookie from better-auth.state to better-auth.oauth_state doesn't work because the formats are fundamentally different. The better-auth.state cookie is a signed string (used in the default/database strategy), while better-auth.oauth_state expects an encrypted JSON payload (used in the cookie/stateless strategy). The OAuth callback tries to decrypt the cookie, so if the payload isn't encrypted as expected, you get decryption errors like "hex string expected" source.

The correct stateless Expo flow relies on the oauth-proxy plugin, which was updated in PR #6131 to support cookie-based state: it encrypts the OAuth state and cookie into the provider URL and restores them on callback, so the cookie format matches what the callback expects. If you still see the mismatch, double-check that the oauth-proxy plugin is active and your flow is using the encrypted state package (not just the raw state string). You should see the state parameter in the provider URL replaced with a long encrypted blob, not a plain string.

If the proxy is not active or misconfigured, the Expo endpoint will only ever have the plain state string and can't fabricate the encrypted payload needed for stateless mode source. The only way to fix this is to ensure the oauth-proxy plugin is enabled and working with stateless mode, so it handles the encryption and cookie restoration for you source.

If you want to debug further, inspect the provider URL and callback parameters for the encrypted state package, and check your plugin config. If the bug persists, it may be worth reporting with logs showing the provider URL, Set-Cookie headers, and callback request details.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3669240133 --> @dosubot[bot] commented on GitHub (Dec 18, 2025): <!-- Answer --> You're right—just renaming the cookie from `better-auth.state` to `better-auth.oauth_state` doesn't work because the formats are fundamentally different. The `better-auth.state` cookie is a signed string (used in the default/database strategy), while `better-auth.oauth_state` expects an encrypted JSON payload (used in the cookie/stateless strategy). The OAuth callback tries to decrypt the cookie, so if the payload isn't encrypted as expected, you get decryption errors like "hex string expected" [source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/oauth2/state.ts#L49-L210). The correct stateless Expo flow relies on the oauth-proxy plugin, which was updated in [PR #6131](https://github.com/better-auth/better-auth/pull/6131) to support cookie-based state: it encrypts the OAuth state and cookie into the provider URL and restores them on callback, so the cookie format matches what the callback expects. If you still see the mismatch, double-check that the oauth-proxy plugin is active and your flow is using the encrypted state package (not just the raw state string). You should see the `state` parameter in the provider URL replaced with a long encrypted blob, not a plain string. If the proxy is not active or misconfigured, the Expo endpoint will only ever have the plain state string and can't fabricate the encrypted payload needed for stateless mode [source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/expo/src/routes.ts#L5-L33). The only way to fix this is to ensure the oauth-proxy plugin is enabled and working with stateless mode, so it handles the encryption and cookie restoration for you [source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/plugins/oauth-proxy/index.ts#L89-L498). If you want to debug further, inspect the provider URL and callback parameters for the encrypted state package, and check your plugin config. If the bug persists, it may be worth reporting with logs showing the provider URL, Set-Cookie headers, and callback request details. <!-- 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/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/801bf405-d14b-4fae-acbd-d40c947fc836?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%27re%20right%E2%80%94just%20renaming%20the%20cookie%20from%20%60better-auth.state%60%20to%20%60better-auth.oauth_state%60%20doesn%27t%20work%20because%20the%20formats%20are%20fundamentally%20different.%20The%20%60better-auth.state%60%20cookie%20is%20a%20signed%20string%20%28used%20in%20the%20default/database%20strategy%29%2C%20while%20%60better-auth.oauth_state%60%20expects%20an%20encrypted%20JSON%20payload%20%28used%20in%20the%20cookie/stateless%20strategy%29.%20The%20OAuth%20callback%20tries%20to%20decrypt%20the%20cookie%2C%20so%20if%20the%20payload%20isn%27t%20encrypted%20as%20expected%2C%20you%20get%20decryption%20errors%20like%20%22hex%20string%20expected%22%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/oauth2/state.ts%23L49-L210%29.%0A%0AThe%20correct%20stateless%20Expo%20flow%20relies%20on%20the%20oauth-proxy%20plugin%2C%20which%20was%20updated%20in%20%5BPR%20%236131%5D%28https%3A//github.com/better-auth/better-auth/pull/6131%29%20to%20support%20cookie-based%20state%3A%20it%20encrypts%20the%20OAuth%20state%20and%20cookie%20into%20the%20provider%20URL%20and%20restores%20them%20on%20callback%2C%20so%20the%20cookie%20format%20matches%20what%20the%20callback%20expects.%20If%20you%20still%20see%20the%20mismatch%2C%20double-check%20that%20the%20oauth-proxy%20plugin%20is%20active%20and%20your%20flow%20is%20using%20the%20encrypted%20state%20package%20%28not%20just%20the%20raw%20state%20string%29.%20You%20should%20see%20the%20%60state%60%20parameter%20in%20the%20provider%20URL%20replaced%20with%20a%20long%20encrypted%20blob%2C%20not%20a%20plain%20string.%0A%0AIf%20the%20proxy%20is%20not%20active%20or%20misconfigured%2C%20the%20Expo%20endpoint%20will%20only%20ever%20have%20the%20plain%20state%20string%20and%20can%27t%20fabricate%20the%20encrypted%20payload%20needed%20for%20stateless%20mode%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/expo/src/routes.ts%23L5-L33%29.%20The%20only%20way%20to%20fix%20this%20is%20to%20ensure%20the%20oauth-proxy%20plugin%20is%20enabled%20and%20working%20with%20stateless%20mode%2C%20so%20it%20handles%20the%20encryption%20and%20cookie%20restoration%20for%20you%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/plugins/oauth-proxy/index.ts%23L89-L498%29.%0A%0AIf%20you%20want%20to%20debug%20further%2C%20inspect%20the%20provider%20URL%20and%20callback%20parameters%20for%20the%20encrypted%20state%20package%2C%20and%20check%20your%20plugin%20config.%20If%20the%20bug%20persists%2C%20it%20may%20be%20worth%20reporting%20with%20logs%20showing%20the%20provider%20URL%2C%20Set-Cookie%20headers%2C%20and%20callback%20request%20details.)&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/6847)
Author
Owner

@ruff-exec commented on GitHub (Dec 18, 2025):

@dosu Does oAuthProxy() apply to Generic OAuth (signIn.oauth2) and the Expo expo-authorization-proxy endpoint? Or only to signIn.social / socialProviders? From my perspective we are talking about 2 different things as I am using the generic OAuth plugin with keycloak and no social providers

<!-- gh-comment-id:3669323825 --> @ruff-exec commented on GitHub (Dec 18, 2025): @dosu Does oAuthProxy() apply to Generic OAuth (signIn.oauth2) and the Expo expo-authorization-proxy endpoint? Or only to signIn.social / socialProviders? From my perspective we are talking about 2 different things as I am using the generic OAuth plugin with keycloak and no social providers
Author
Owner

@dosubot[bot] commented on GitHub (Dec 18, 2025):

Great question—these are indeed two different mechanisms.

  • The oauthProxy() plugin applies to both generic OAuth flows (signIn.oauth2, e.g. Keycloak) and social providers (signIn.social). Its hooks explicitly match both /sign-in/oauth2 and /sign-in/social endpoints, so it supports stateless/cookie-based OAuth for generic providers as well as social ones. This means if you enable oauthProxy(), it will handle encrypted state/cookie packaging for your Keycloak flow, not just Google/Facebook/etc. (source)

  • The Expo /expo-authorization-proxy endpoint is a separate implementation in the Expo plugin. It does not use or integrate with oauthProxy(). It always sets a signed better-auth.state cookie with the raw state string, regardless of your storeStateStrategy (source). This is why you see cookie format mismatches in stateless mode—the Expo proxy doesn't handle the encrypted payload required by oauthProxy().

So:

  • oauthProxy() is relevant and necessary for stateless/cookie-based OAuth flows with generic providers (like Keycloak) in Expo/native environments.
  • The Expo proxy endpoint is a workaround for mobile cookie limitations, but it does not support encrypted state/cookie payloads needed for stateless mode.

If you want stateless OAuth to work in Expo with generic providers, you need to enable and configure oauthProxy()—the Expo proxy alone won't suffice.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3669337072 --> @dosubot[bot] commented on GitHub (Dec 18, 2025): <!-- Answer --> Great question—these are indeed two different mechanisms. - The `oauthProxy()` plugin applies to both generic OAuth flows (`signIn.oauth2`, e.g. Keycloak) and social providers (`signIn.social`). Its hooks explicitly match both `/sign-in/oauth2` and `/sign-in/social` endpoints, so it supports stateless/cookie-based OAuth for generic providers as well as social ones. This means if you enable `oauthProxy()`, it will handle encrypted state/cookie packaging for your Keycloak flow, not just Google/Facebook/etc. ([source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/plugins/oauth-proxy/index.ts#L48-L498)) - The Expo `/expo-authorization-proxy` endpoint is a separate implementation in the Expo plugin. It does not use or integrate with `oauthProxy()`. It always sets a signed `better-auth.state` cookie with the raw state string, regardless of your `storeStateStrategy` ([source](https://github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/expo/src/routes.ts#L5-L34)). This is why you see cookie format mismatches in stateless mode—the Expo proxy doesn't handle the encrypted payload required by `oauthProxy()`. So: - `oauthProxy()` is relevant and necessary for stateless/cookie-based OAuth flows with generic providers (like Keycloak) in Expo/native environments. - The Expo proxy endpoint is a workaround for mobile cookie limitations, but it does not support encrypted state/cookie payloads needed for stateless mode. If you want stateless OAuth to work in Expo with generic providers, you need to enable and configure `oauthProxy()`—the Expo proxy alone won't suffice. <!-- 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/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1495ae0e-b328-4080-8e5a-d5c2feadc987?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Great%20question%E2%80%94these%20are%20indeed%20two%20different%20mechanisms.%0A%0A-%20The%20%60oauthProxy%28%29%60%20plugin%20applies%20to%20both%20generic%20OAuth%20flows%20%28%60signIn.oauth2%60%2C%20e.g.%20Keycloak%29%20and%20social%20providers%20%28%60signIn.social%60%29.%20Its%20hooks%20explicitly%20match%20both%20%60/sign-in/oauth2%60%20and%20%60/sign-in/social%60%20endpoints%2C%20so%20it%20supports%20stateless/cookie-based%20OAuth%20for%20generic%20providers%20as%20well%20as%20social%20ones.%20This%20means%20if%20you%20enable%20%60oauthProxy%28%29%60%2C%20it%20will%20handle%20encrypted%20state/cookie%20packaging%20for%20your%20Keycloak%20flow%2C%20not%20just%20Google/Facebook/etc.%20%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/better-auth/src/plugins/oauth-proxy/index.ts%23L48-L498%29%29%0A%0A-%20The%20Expo%20%60/expo-authorization-proxy%60%20endpoint%20is%20a%20separate%20implementation%20in%20the%20Expo%20plugin.%20It%20does%20not%20use%20or%20integrate%20with%20%60oauthProxy%28%29%60.%20It%20always%20sets%20a%20signed%20%60better-auth.state%60%20cookie%20with%20the%20raw%20state%20string%2C%20regardless%20of%20your%20%60storeStateStrategy%60%20%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/91cea0d0a79e6009eccda62e43c39cb44ef59625/packages/expo/src/routes.ts%23L5-L34%29%29.%20This%20is%20why%20you%20see%20cookie%20format%20mismatches%20in%20stateless%20mode%E2%80%94the%20Expo%20proxy%20doesn%27t%20handle%20the%20encrypted%20payload%20required%20by%20%60oauthProxy%28%29%60.%0A%0ASo%3A%20%20%0A-%20%60oauthProxy%28%29%60%20is%20relevant%20and%20necessary%20for%20stateless/cookie-based%20OAuth%20flows%20with%20generic%20providers%20%28like%20Keycloak%29%20in%20Expo/native%20environments.%0A-%20The%20Expo%20proxy%20endpoint%20is%20a%20workaround%20for%20mobile%20cookie%20limitations%2C%20but%20it%20does%20not%20support%20encrypted%20state/cookie%20payloads%20needed%20for%20stateless%20mode.%0A%0AIf%20you%20want%20stateless%20OAuth%20to%20work%20in%20Expo%20with%20generic%20providers%2C%20you%20need%20to%20enable%20and%20configure%20%60oauthProxy%28%29%60%E2%80%94the%20Expo%20proxy%20alone%20won%27t%20suffice.)&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/6847)
Author
Owner

@ruff-exec commented on GitHub (Dec 18, 2025):

@dosu Ok if such a configuration is required how would it look like? How do I need to modify my current auth config?

<!-- gh-comment-id:3670148456 --> @ruff-exec commented on GitHub (Dec 18, 2025): @dosu Ok if such a configuration is required how would it look like? How do I need to modify my current auth config?
Author
Owner

@dosubot[bot] commented on GitHub (Dec 18, 2025):

To enable stateless OAuth with Expo and genericOAuth (Keycloak), you need to add the oAuthProxy() plugin to your Better Auth config. This plugin handles encrypted state/cookie packaging for both social and generic providers, including Keycloak, and works with Expo-native flows. Your config should look like this:

import { betterAuth } from 'better-auth'
import { genericOAuth, keycloak, oAuthProxy } from 'better-auth/plugins'
import { expo } from '@better-auth/expo'

export const auth = betterAuth({
  plugins: [
    expo(),
    oAuthProxy(), // <-- Add this line
    genericOAuth({
      config: [
        keycloak({
          clientId: process.env.KEYCLOAK_CLIENT_ID!,
          clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
          issuer: process.env.KEYCLOAK_ISSUER!,
          scopes: ['openid', 'profile', 'email'],
          pkce: true,
        }),
      ],
    }),
  ],
  trustedOrigins: [
    'com.company.app://',
    ...(process.env.NODE_ENV === 'development'
      ? [
          'exp://*/*',
          'exp://10.0.0.*:*/*',
          'exp://192.168.*.*:*/*',
          'exp://172.*.*.*:*/*',
          'exp://localhost:*/*',
        ]
      : []),
  ],
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 60 * 60 * 2,
      strategy: 'jwt',
      refreshCache: true,
    },
  },
  account: {
    storeStateStrategy: 'cookie',
    storeAccountCookie: true,
    skipStateCookieCheck: true, // Required for proxy plugin
  },
})

No changes are needed on the Expo client side; the proxy plugin operates server-side. You can optionally pass currentURL and productionURL to oAuthProxy() if your URLs are not inferred correctly. The plugin will handle encrypted state/cookie packaging for both /sign-in/oauth2 and /sign-in/social flows, so your Keycloak OAuth should work in Expo-native environments without state mismatch errors.
See plugin docs and code reference for details.

Caveat: skipStateCookieCheck: true has security implications and should only be used in dev/staging.
If you still see issues, double-check your callback URLs and trustedOrigins.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3670161556 --> @dosubot[bot] commented on GitHub (Dec 18, 2025): <!-- Answer --> To enable stateless OAuth with Expo and genericOAuth (Keycloak), you need to add the `oAuthProxy()` plugin to your Better Auth config. This plugin handles encrypted state/cookie packaging for both social and generic providers, including Keycloak, and works with Expo-native flows. Your config should look like this: ```typescript import { betterAuth } from 'better-auth' import { genericOAuth, keycloak, oAuthProxy } from 'better-auth/plugins' import { expo } from '@better-auth/expo' export const auth = betterAuth({ plugins: [ expo(), oAuthProxy(), // <-- Add this line genericOAuth({ config: [ keycloak({ clientId: process.env.KEYCLOAK_CLIENT_ID!, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, issuer: process.env.KEYCLOAK_ISSUER!, scopes: ['openid', 'profile', 'email'], pkce: true, }), ], }), ], trustedOrigins: [ 'com.company.app://', ...(process.env.NODE_ENV === 'development' ? [ 'exp://*/*', 'exp://10.0.0.*:*/*', 'exp://192.168.*.*:*/*', 'exp://172.*.*.*:*/*', 'exp://localhost:*/*', ] : []), ], session: { cookieCache: { enabled: true, maxAge: 60 * 60 * 2, strategy: 'jwt', refreshCache: true, }, }, account: { storeStateStrategy: 'cookie', storeAccountCookie: true, skipStateCookieCheck: true, // Required for proxy plugin }, }) ``` No changes are needed on the Expo client side; the proxy plugin operates server-side. You can optionally pass `currentURL` and `productionURL` to `oAuthProxy()` if your URLs are not inferred correctly. The plugin will handle encrypted state/cookie packaging for both `/sign-in/oauth2` and `/sign-in/social` flows, so your Keycloak OAuth should work in Expo-native environments without state mismatch errors. See [plugin docs](https://github.com/better-auth/better-auth/blob/6e6735635519e6900e981b5d7857e8e717d46104/docs/content/docs/plugins/oauth-proxy.mdx#L6-L67) and [code reference](https://github.com/better-auth/better-auth/blob/6e6735635519e6900e981b5d7857e8e717d46104/packages/better-auth/src/plugins/oauth-proxy/index.ts#L16-L503) for details. **Caveat:** `skipStateCookieCheck: true` has security implications and should only be used in dev/staging. If you still see issues, double-check your callback URLs and trustedOrigins. <!-- 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/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/d4f11532-47b6-4904-8a5c-bfa3e460ee19?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=To%20enable%20stateless%20OAuth%20with%20Expo%20and%20genericOAuth%20%28Keycloak%29%2C%20you%20need%20to%20add%20the%20%60oAuthProxy%28%29%60%20plugin%20to%20your%20Better%20Auth%20config.%20This%20plugin%20handles%20encrypted%20state/cookie%20packaging%20for%20both%20social%20and%20generic%20providers%2C%20including%20Keycloak%2C%20and%20works%20with%20Expo-native%20flows.%20Your%20config%20should%20look%20like%20this%3A%0A%0A%60%60%60typescript%0Aimport%20%7B%20betterAuth%20%7D%20from%20%27better-auth%27%0Aimport%20%7B%20genericOAuth%2C%20keycloak%2C%20oAuthProxy%20%7D%20from%20%27better-auth/plugins%27%0Aimport%20%7B%20expo%20%7D%20from%20%27%40better-auth/expo%27%0A%0Aexport%20const%20auth%20%3D%20betterAuth%28%7B%0A%20%20plugins%3A%20%5B%0A%20%20%20%20expo%28%29%2C%0A%20%20%20%20oAuthProxy%28%29%2C%20//%20%3C--%20Add%20this%20line%0A%20%20%20%20genericOAuth%28%7B%0A%20%20%20%20%20%20config%3A%20%5B%0A%20%20%20%20%20%20%20%20keycloak%28%7B%0A%20%20%20%20%20%20%20%20%20%20clientId%3A%20process.env.KEYCLOAK_CLIENT_ID%21%2C%0A%20%20%20%20%20%20%20%20%20%20clientSecret%3A%20process.env.KEYCLOAK_CLIENT_SECRET%21%2C%0A%20%20%20%20%20%20%20%20%20%20issuer%3A%20process.env.KEYCLOAK_ISSUER%21%2C%0A%20%20%20%20%20%20%20%20%20%20scopes%3A%20%5B%27openid%27%2C%20%27profile%27%2C%20%27email%27%5D%2C%0A%20%20%20%20%20%20%20%20%20%20pkce%3A%20true%2C%0A%20%20%20%20%20%20%20%20%7D%29%2C%0A%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%7D%29%2C%0A%20%20%5D%2C%0A%20%20trustedOrigins%3A%20%5B%0A%20%20%20%20%27com.company.app%3A//%27%2C%0A%20%20%20%20...%28process.env.NODE_ENV%20%3D%3D%3D%20%27development%27%0A%20%20%20%20%20%20%3F%20%5B%0A%20%20%20%20%20%20%20%20%20%20%27exp%3A//%2A/%2A%27%2C%0A%20%20%20%20%20%20%20%20%20%20%27exp%3A//10.0.0.%2A%3A%2A/%2A%27%2C%0A%20%20%20%20%20%20%20%20%20%20%27exp%3A//192.168.%2A.%2A%3A%2A/%2A%27%2C%0A%20%20%20%20%20%20%20%20%20%20%27exp%3A//172.%2A.%2A.%2A%3A%2A/%2A%27%2C%0A%20%20%20%20%20%20%20%20%20%20%27exp%3A//localhost%3A%2A/%2A%27%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%3A%20%5B%5D%29%2C%0A%20%20%5D%2C%0A%20%20session%3A%20%7B%0A%20%20%20%20cookieCache%3A%20%7B%0A%20%20%20%20%20%20enabled%3A%20true%2C%0A%20%20%20%20%20%20maxAge%3A%2060%20%2A%2060%20%2A%202%2C%0A%20%20%20%20%20%20strategy%3A%20%27jwt%27%2C%0A%20%20%20%20%20%20refreshCache%3A%20true%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%2C%0A%20%20account%3A%20%7B%0A%20%20%20%20storeStateStrategy%3A%20%27cookie%27%2C%0A%20%20%20%20storeAccountCookie%3A%20true%2C%0A%20%20%20%20skipStateCookieCheck%3A%20true%2C%20//%20Required%20for%20proxy%20plugin%0A%20%20%7D%2C%0A%7D%29%0A%60%60%60%0A%0ANo%20changes%20are%20needed%20on%20the%20Expo%20client%20side%3B%20the%20proxy%20plugin%20operates%20server-side.%20You%20can%20optionally%20pass%20%60currentURL%60%20and%20%60productionURL%60%20to%20%60oAuthProxy%28%29%60%20if%20your%20URLs%20are%20not%20inferred%20correctly.%20The%20plugin%20will%20handle%20encrypted%20state/cookie%20packaging%20for%20both%20%60/sign-in/oauth2%60%20and%20%60/sign-in/social%60%20flows%2C%20so%20your%20Keycloak%20OAuth%20should%20work%20in%20Expo-native%20environments%20without%20state%20mismatch%20errors.%20%20%0ASee%20%5Bplugin%20docs%5D%28https%3A//github.com/better-auth/better-auth/blob/6e6735635519e6900e981b5d7857e8e717d46104/docs/content/docs/plugins/oauth-proxy.mdx%23L6-L67%29%20and%20%5Bcode%20reference%5D%28https%3A//github.com/better-auth/better-auth/blob/6e6735635519e6900e981b5d7857e8e717d46104/packages/better-auth/src/plugins/oauth-proxy/index.ts%23L16-L503%29%20for%20details.%0A%0A%2A%2ACaveat%3A%2A%2A%20%60skipStateCookieCheck%3A%20true%60%20has%20security%20implications%20and%20should%20only%20be%20used%20in%20dev/staging.%20%20%0AIf%20you%20still%20see%20issues%2C%20double-check%20your%20callback%20URLs%20and%20trustedOrigins.)&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/6847)
Author
Owner

@ruff-exec commented on GitHub (Dec 18, 2025):

@dosu I still see the issue

Here is the log from the api route:

Android Bundled 379ms node_modules/expo-network/build/Network.js (613 modules)
RSC(Web) Bundled 727ms src/app/api/auth/[...auth]+api.ts (596 modules)
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/get-session
Cookie header: ; better-auth.oauth_state=0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7
BetterAuth cookies: {
  'better-auth.oauth_state': '0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: POST
Request URL: http://localhost:8081/api/auth/sign-in/oauth2
Cookie header: ; better-auth.oauth_state=0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7
BetterAuth cookies: {
  'better-auth.oauth_state': '0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7'
}
Set-Cookie header: better-auth.oauth_state=9f8bbd6063cc6af5b7c2b02aba20c414129c049678016c5d43cb19b4850c24edf8105ff39a01f9903a4204a247bb1785908c0900cfbafc2f260cb7620ef511e37ce4e2f8d742737a5ffbd90f362d6a19935fa8f9a08ab8c5f6993cf2284526af8cc37a2f5408ada64b7af16436822482639c708555c69c8770219c76919f2fa5b000609133875ac40bb7bbfb26e4cf8283b2166ba1f731e102d6e185375a25298356d3d93867584b1e74b3ec828c7acb78a8077e5f570fe67fe5151a3f830ccc7abecbd678152f77f0a5b586ad9dfc9f022f8c916c11c09102b481188a4954e1cfaeb984a7df12bcf627e38c8c4f28bfc58c5fdb6489b00301e684cc8365192787ff312f9f4c381886ac1100891841fad19a6b51ab1044e190ca86b36547475e84207a0fe3edd842d9c7673da95766ee; Max-Age=600000; Path=/; HttpOnly; SameSite=Lax
Set-Cookie names: [ 'better-auth.oauth_state' ]
============================
Android Bundled 347ms node_modules/expo-web-browser/build/WebBrowser.js (619 modules)
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/get-session
Cookie header: ; better-auth.oauth_state=9f8bbd6063cc6af5b7c2b02aba20c414129c049678016c5d43cb19b4850c24edf8105ff39a01f9903a4204a247bb1785908c0900cfbafc2f260cb7620ef511e37ce4e2f8d742737a5ffbd90f362d6a19935fa8f9a08ab8c5f6993cf2284526af8cc37a2f5408ada64b7af16436822482639c708555c69c8770219c76919f2fa5b000609133875ac40bb7bbfb26e4cf8283b2166ba1f731e102d6e185375a25298356d3d93867584b1e74b3ec828c7acb78a8077e5f570fe67fe5151a3f830ccc7abecbd678152f77f0a5b586ad9dfc9f022f8c916c11c09102b481188a4954e1cfaeb984a7df12bcf627e38c8c4f28bfc58c5fdb6489b00301e684cc8365192787ff312f9f4c381886ac1100891841fad19a6b51ab1044e190ca86b36547475e84207a0fe3edd842d9c7673da95766ee
BetterAuth cookies: {
  'better-auth.oauth_state': '9f8bbd6063cc6af5b7c2b02aba20c414129c049678016c5d43cb19b4850c24edf8105ff39a01f9903a4204a247bb1785908c0900cfbafc2f260cb7620ef511e37ce4e2f8d742737a5ffbd90f362d6a19935fa8f9a08ab8c5f6993cf2284526af8cc37a2f5408ada64b7af16436822482639c708555c69c8770219c76919f2fa5b000609133875ac40bb7bbfb26e4cf8283b2166ba1f731e102d6e185375a25298356d3d93867584b1e74b3ec828c7acb78a8077e5f570fe67fe5151a3f830ccc7abecbd678152f77f0a5b586ad9dfc9f022f8c916c11c09102b481188a4954e1cfaeb984a7df12bcf627e38c8c4f28bfc58c5fdb6489b00301e684cc8365192787ff312f9f4c381886ac1100891841fad19a6b51ab1044e190ca86b36547475e84207a0fe3edd842d9c7673da95766ee'
}
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/expo-authorization-proxy?authorizationURL=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse%2Fprotocol%2Fopenid-connect%2Fauth%3Fresponse_type%3Dcode%26client_id%3Dmobile-ilogistics-expo%26state%3DCoXRlJp_znRjDUatUMi7bfSRhneUuRqx%26scope%3Dopenid%2Bprofile%2Bemail%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8081%252Fapi%252Fauth%252Foauth2%252Fcallback%252Fkeycloak%26code_challenge_method%3DS256%26code_challenge%3D7oD2huVVNBbRRKXM9oGTWRzwl37CmBZnOYlgUzxDM6s
Cookie header: null
BetterAuth cookies: {}
Set-Cookie header: better-auth.state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D; Max-Age=300000; Path=/; HttpOnly; SameSite=Lax
Set-Cookie names: [ 'better-auth.state' ]
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/oauth2/callback/keycloak?state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx&session_state=db5c42d4-d6e5-46e8-a303-ab0d261b647f&iss=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse&code=52a43de7-176f-4d2c-8720-300418c57943.db5c42d4-d6e5-46e8-a303-ab0d261b647f.4c9fcf16-e1e3-4936-a511-298d877cd55f
Cookie header: better-auth.state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D
BetterAuth cookies: {
  'better-auth.state': 'CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D'
}
2025-12-18T13:05:54.532Z ERROR [Better Auth]: State Mismatch. OAuth state cookie not found { state: 'CoXRlJp_znRjDUatUMi7bfSRhneUuRqx' }
Set-Cookie header: <none>
============================
=== BetterAuth API Route ===
Method: GET
Request URL: http://localhost:8081/api/auth/error?error=please_restart_the_process
Cookie header: better-auth.state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D
BetterAuth cookies: {
  'better-auth.state': 'CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D'
}
Set-Cookie header: <none>
============================
<!-- gh-comment-id:3670226649 --> @ruff-exec commented on GitHub (Dec 18, 2025): @dosu I still see the issue Here is the log from the api route: ```txt Android Bundled 379ms node_modules/expo-network/build/Network.js (613 modules) RSC(Web) Bundled 727ms src/app/api/auth/[...auth]+api.ts (596 modules) === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/get-session Cookie header: ; better-auth.oauth_state=0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7 BetterAuth cookies: { 'better-auth.oauth_state': '0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: POST Request URL: http://localhost:8081/api/auth/sign-in/oauth2 Cookie header: ; better-auth.oauth_state=0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7 BetterAuth cookies: { 'better-auth.oauth_state': '0e5b44b54d3ac6dca3ee4cd8bb02d66fc1079e334d66ff9be1b7c1c39df360039de788f7a199bc736a9cbd5ee0c6ab67826aaed8c61bb25592ff89594edbb5e78e0285c7dbdf86fca6ebdcb7adb9b9ae7fda6c14abe290e909b7f5177ef4c0432f0e14c7f3f38cbbc5fd1efcb2c6138ddc60f42cb9220c794ec1e203546cc7ee24355d7c925843e87e5d462ddfeda8da304108ed2cee25cc75c40a7ad8f4103663f0f70d803b229547e8760d42b54f61c31cdbafb0c7a5723da70551f8ec0e67a65d77baf1461c990baec8c11f05890b204f1cbeca35c5e5168551f4b29aa21e3ba5c2755a17ab5c0f7514566723be211f44f5ef0330734404bed7518325ae1e2801ea9c2b098c26727d9de118ba95b7521f1b018e4a9094fb8a43720aec975339fadce90ae1aa45c4b1c2a0ba4c00f7' } Set-Cookie header: better-auth.oauth_state=9f8bbd6063cc6af5b7c2b02aba20c414129c049678016c5d43cb19b4850c24edf8105ff39a01f9903a4204a247bb1785908c0900cfbafc2f260cb7620ef511e37ce4e2f8d742737a5ffbd90f362d6a19935fa8f9a08ab8c5f6993cf2284526af8cc37a2f5408ada64b7af16436822482639c708555c69c8770219c76919f2fa5b000609133875ac40bb7bbfb26e4cf8283b2166ba1f731e102d6e185375a25298356d3d93867584b1e74b3ec828c7acb78a8077e5f570fe67fe5151a3f830ccc7abecbd678152f77f0a5b586ad9dfc9f022f8c916c11c09102b481188a4954e1cfaeb984a7df12bcf627e38c8c4f28bfc58c5fdb6489b00301e684cc8365192787ff312f9f4c381886ac1100891841fad19a6b51ab1044e190ca86b36547475e84207a0fe3edd842d9c7673da95766ee; Max-Age=600000; Path=/; HttpOnly; SameSite=Lax Set-Cookie names: [ 'better-auth.oauth_state' ] ============================ Android Bundled 347ms node_modules/expo-web-browser/build/WebBrowser.js (619 modules) === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/get-session Cookie header: ; better-auth.oauth_state=9f8bbd6063cc6af5b7c2b02aba20c414129c049678016c5d43cb19b4850c24edf8105ff39a01f9903a4204a247bb1785908c0900cfbafc2f260cb7620ef511e37ce4e2f8d742737a5ffbd90f362d6a19935fa8f9a08ab8c5f6993cf2284526af8cc37a2f5408ada64b7af16436822482639c708555c69c8770219c76919f2fa5b000609133875ac40bb7bbfb26e4cf8283b2166ba1f731e102d6e185375a25298356d3d93867584b1e74b3ec828c7acb78a8077e5f570fe67fe5151a3f830ccc7abecbd678152f77f0a5b586ad9dfc9f022f8c916c11c09102b481188a4954e1cfaeb984a7df12bcf627e38c8c4f28bfc58c5fdb6489b00301e684cc8365192787ff312f9f4c381886ac1100891841fad19a6b51ab1044e190ca86b36547475e84207a0fe3edd842d9c7673da95766ee BetterAuth cookies: { 'better-auth.oauth_state': '9f8bbd6063cc6af5b7c2b02aba20c414129c049678016c5d43cb19b4850c24edf8105ff39a01f9903a4204a247bb1785908c0900cfbafc2f260cb7620ef511e37ce4e2f8d742737a5ffbd90f362d6a19935fa8f9a08ab8c5f6993cf2284526af8cc37a2f5408ada64b7af16436822482639c708555c69c8770219c76919f2fa5b000609133875ac40bb7bbfb26e4cf8283b2166ba1f731e102d6e185375a25298356d3d93867584b1e74b3ec828c7acb78a8077e5f570fe67fe5151a3f830ccc7abecbd678152f77f0a5b586ad9dfc9f022f8c916c11c09102b481188a4954e1cfaeb984a7df12bcf627e38c8c4f28bfc58c5fdb6489b00301e684cc8365192787ff312f9f4c381886ac1100891841fad19a6b51ab1044e190ca86b36547475e84207a0fe3edd842d9c7673da95766ee' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/expo-authorization-proxy?authorizationURL=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse%2Fprotocol%2Fopenid-connect%2Fauth%3Fresponse_type%3Dcode%26client_id%3Dmobile-ilogistics-expo%26state%3DCoXRlJp_znRjDUatUMi7bfSRhneUuRqx%26scope%3Dopenid%2Bprofile%2Bemail%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8081%252Fapi%252Fauth%252Foauth2%252Fcallback%252Fkeycloak%26code_challenge_method%3DS256%26code_challenge%3D7oD2huVVNBbRRKXM9oGTWRzwl37CmBZnOYlgUzxDM6s Cookie header: null BetterAuth cookies: {} Set-Cookie header: better-auth.state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D; Max-Age=300000; Path=/; HttpOnly; SameSite=Lax Set-Cookie names: [ 'better-auth.state' ] ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/oauth2/callback/keycloak?state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx&session_state=db5c42d4-d6e5-46e8-a303-ab0d261b647f&iss=https%3A%2F%2Fkeycloak.ilogistics.at%2Fauth%2Frealms%2Fse&code=52a43de7-176f-4d2c-8720-300418c57943.db5c42d4-d6e5-46e8-a303-ab0d261b647f.4c9fcf16-e1e3-4936-a511-298d877cd55f Cookie header: better-auth.state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D BetterAuth cookies: { 'better-auth.state': 'CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D' } 2025-12-18T13:05:54.532Z ERROR [Better Auth]: State Mismatch. OAuth state cookie not found { state: 'CoXRlJp_znRjDUatUMi7bfSRhneUuRqx' } Set-Cookie header: <none> ============================ === BetterAuth API Route === Method: GET Request URL: http://localhost:8081/api/auth/error?error=please_restart_the_process Cookie header: better-auth.state=CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D BetterAuth cookies: { 'better-auth.state': 'CoXRlJp_znRjDUatUMi7bfSRhneUuRqx.vqBPnQjPMMHisRivwiJOY1sN8IgBYW0SJsReKy8vVWc%3D' } Set-Cookie header: <none> ============================ ```
Author
Owner

@dosubot[bot] commented on GitHub (Dec 18, 2025):

The persistent state mismatch is happening because the Expo plugin’s /api/auth/expo-authorization-proxy endpoint is not integrated with the oauthProxy plugin and does not support stateless/cookie-based OAuth flows for generic providers. This endpoint always sets a signed better-auth.state cookie with the raw state string, regardless of your storeStateStrategy, while the stateless flow expects an encrypted better-auth.oauth_state cookie. The oauthProxy plugin only intercepts /sign-in/oauth2, /sign-in/social, and callback routes—not /expo-authorization-proxy—so the cookie format mismatch persists in Expo-native flows using genericOAuth with stateless mode. This is a current architectural limitation, not a misconfiguration or a versioning issue (Expo proxy code, oauthProxy code).

There’s no official workaround for stateless OAuth with Expo-native and generic providers until the Expo plugin is updated to support encrypted state cookies or is integrated with oauthProxy. For now, you can either revert to the default (database) state strategy for Expo-native flows or consider forking/extending the Expo plugin to handle the encrypted state format. If your use case is blocked, it’s worth documenting this limitation for others and monitoring for updates or feature requests in the repo.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3670251347 --> @dosubot[bot] commented on GitHub (Dec 18, 2025): <!-- Answer --> The persistent state mismatch is happening because the Expo plugin’s /api/auth/expo-authorization-proxy endpoint is not integrated with the oauthProxy plugin and does not support stateless/cookie-based OAuth flows for generic providers. This endpoint always sets a signed better-auth.state cookie with the raw state string, regardless of your storeStateStrategy, while the stateless flow expects an encrypted better-auth.oauth_state cookie. The oauthProxy plugin only intercepts /sign-in/oauth2, /sign-in/social, and callback routes—not /expo-authorization-proxy—so the cookie format mismatch persists in Expo-native flows using genericOAuth with stateless mode. This is a current architectural limitation, not a misconfiguration or a versioning issue ([Expo proxy code](https://github.com/better-auth/better-auth/blob/c01b867bd4caa4b538c3d164a3acbe4a52cfd141/packages/expo/src/routes.ts#L5-L34), [oauthProxy code](https://github.com/better-auth/better-auth/blob/c01b867bd4caa4b538c3d164a3acbe4a52cfd141/packages/better-auth/src/plugins/oauth-proxy/index.ts#L43-L498)). There’s no official workaround for stateless OAuth with Expo-native and generic providers until the Expo plugin is updated to support encrypted state cookies or is integrated with oauthProxy. For now, you can either revert to the default (database) state strategy for Expo-native flows or consider forking/extending the Expo plugin to handle the encrypted state format. If your use case is blocked, it’s worth documenting this limitation for others and monitoring for updates or feature requests in the repo. <!-- 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/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ec4670fd-10b7-4f76-a3f3-93cb77629b10?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=The%20persistent%20state%20mismatch%20is%20happening%20because%20the%20Expo%20plugin%E2%80%99s%20/api/auth/expo-authorization-proxy%20endpoint%20is%20not%20integrated%20with%20the%20oauthProxy%20plugin%20and%20does%20not%20support%20stateless/cookie-based%20OAuth%20flows%20for%20generic%20providers.%20This%20endpoint%20always%20sets%20a%20signed%20better-auth.state%20cookie%20with%20the%20raw%20state%20string%2C%20regardless%20of%20your%20storeStateStrategy%2C%20while%20the%20stateless%20flow%20expects%20an%20encrypted%20better-auth.oauth_state%20cookie.%20The%20oauthProxy%20plugin%20only%20intercepts%20/sign-in/oauth2%2C%20/sign-in/social%2C%20and%20callback%20routes%E2%80%94not%20/expo-authorization-proxy%E2%80%94so%20the%20cookie%20format%20mismatch%20persists%20in%20Expo-native%20flows%20using%20genericOAuth%20with%20stateless%20mode.%20This%20is%20a%20current%20architectural%20limitation%2C%20not%20a%20misconfiguration%20or%20a%20versioning%20issue%20%28%5BExpo%20proxy%20code%5D%28https%3A//github.com/better-auth/better-auth/blob/c01b867bd4caa4b538c3d164a3acbe4a52cfd141/packages/expo/src/routes.ts%23L5-L34%29%2C%20%5BoauthProxy%20code%5D%28https%3A//github.com/better-auth/better-auth/blob/c01b867bd4caa4b538c3d164a3acbe4a52cfd141/packages/better-auth/src/plugins/oauth-proxy/index.ts%23L43-L498%29%29.%0A%0AThere%E2%80%99s%20no%20official%20workaround%20for%20stateless%20OAuth%20with%20Expo-native%20and%20generic%20providers%20until%20the%20Expo%20plugin%20is%20updated%20to%20support%20encrypted%20state%20cookies%20or%20is%20integrated%20with%20oauthProxy.%20For%20now%2C%20you%20can%20either%20revert%20to%20the%20default%20%28database%29%20state%20strategy%20for%20Expo-native%20flows%20or%20consider%20forking/extending%20the%20Expo%20plugin%20to%20handle%20the%20encrypted%20state%20format.%20If%20your%20use%20case%20is%20blocked%2C%20it%E2%80%99s%20worth%20documenting%20this%20limitation%20for%20others%20and%20monitoring%20for%20updates%20or%20feature%20requests%20in%20the%20repo.)&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/6847)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27975