Better Auth OIDC Provider - Userinfo Endpoint Returns Incomplete Data #1798

Closed
opened 2026-03-13 09:04:10 -05:00 by GiteaMirror · 11 comments
Owner

Originally created by @JustKira on GitHub (Aug 27, 2025).

Issue Summary

When using better-auth's OIDC provider plugin with the genericOAuth plugin as client, the userinfo endpoint returns incomplete user data (missing email, name, emailVerified fields) even though a valid user exists in the database with complete information.

Environment

  • better-auth version: 1.3.7
  • Database adapters tested:
    • PostgreSQL (using pg adapter)
    • SurrealDB (using custom better-auth-surrealdb adapter v1.0.1)
  • Runtime: Bun (latest)
  • Framework: Hono v4.8.4
  • Node environment: Backend API service

Project Structure

This is a monorepo with the following structure:

uni-fied/
├── apps/
│   ├── accounts/     # SvelteKit auth UI app
│   ├── backend/      # Hono + better-auth API server
│   ├── platform/     # Platform app
│   └── studio/       # Studio app
└── packages/
    └── api/          # Shared API types

Configuration

Backend Auth Setup (apps/backend/src/routes/auth/auth.ts)

import { betterAuth } from "better-auth";
import { admin, genericOAuth, jwt, oidcProvider, openAPI } from "better-auth/plugins";

export const auth = betterAuth({
  database: new Pool({
    connectionString: env.POSTGRE_URL,
  }),
  emailAndPassword: {
    enabled: true,
  },
  trustedOrigins: env.ALLOWED_ORIGINS.split(","),
  advanced: {
    crossSubDomainCookies: {
      enabled: true,
      domain: env.TRUSTED_SUBDOMAIN_COOKIES,
    },
  },
  plugins: [
    admin(),
    openAPI(),
    jwt(),
    // OAuth Client Configuration
    genericOAuth({
      config: [
        {
          providerId: "unified",
          clientId: env.OAUTH_CLIENT_ID,
          clientSecret: env.OAUTH_CLIENT_SECRET,
          discoveryUrl: `${env.BETTER_AUTH_URL}/api/auth/.well-known/openid-configuration`,
        },
      ],
    }),
    // OIDC Provider Configuration
    oidcProvider({
      loginPage: env.OIDC_LOGIN_PAGE_URL,
      useJWTPlugin: true,
      trustedClients: [
        {
          clientId: env.OAUTH_CLIENT_ID,
          clientSecret: env.OAUTH_CLIENT_SECRET,
          disabled: false,
          metadata: {},
          name: "unified",
          type: "web",
          skipConsent: true,
          redirectURLs: [
            `${env.BETTER_AUTH_URL}/api/auth/oauth2/callback/unified`,
          ],
        },
      ],
    }),
  ],
});

Reproduction Steps

  1. User registers via email/password

    • User successfully creates account at /api/auth/sign-up/email
    • User data is properly stored in database with complete fields (email, name, etc.)
  2. OAuth2 flow initiates

    • Client calls /api/auth/sign-in/oauth2
    • Redirect to authorization endpoint: /api/auth/oauth2/authorize
    • User authenticates successfully
    • Authorization code is generated
  3. Token exchange succeeds

    • Callback to /api/auth/oauth2/callback/unified with authorization code
    • Token endpoint /api/auth/oauth2/token successfully returns access token
  4. Userinfo endpoint fails to return complete data

    • Request to /api/auth/oauth2/userinfo with valid access token
    • Returns incomplete user object

Actual Behavior

Console Output

<-- POST /api/auth/sign-in/oauth2
--> POST /api/auth/sign-in/oauth2 200 4s
<-- GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=...
--> GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=... 302 12ms
<-- POST /api/auth/sign-up/email
--> POST /api/auth/sign-up/email 200 3s
<-- GET /api/auth/oauth2/callback/unified?code=NRINWztknEgYNHg9ikKo9w7gordUlWBD&state=...
<-- POST /api/auth/oauth2/token
--> POST /api/auth/oauth2/token 200 2s
<-- GET /api/auth/oauth2/userinfo
--> GET /api/auth/oauth2/userinfo 200 489ms
2025-08-27T23:11:14.251Z ERROR [Better Auth]: Unable to get user info {
  id: "W0PZ2peuQ4XFjhtJghOPAZ3u6LFLaUSC",
  emailVerified: undefined,
  email: undefined,
  image: undefined,
  name: undefined,
  sub: "W0PZ2peuQ4XFjhtJghOPAZ3u6LFLaUSC",
}
--> GET /api/auth/oauth2/callback/unified?code=... 302 3s
<-- GET /api/auth/error?error=email_is_missing

Expected Behavior

The userinfo endpoint should return complete user data including:

  • email: User's email address
  • name: User's name
  • emailVerified: Email verification status
  • image: User's profile image (if available)
  • id/sub: User identifier (this works correctly)

Additional Context

Database Verification

  • Confirmed: The user exists in the database with all fields populated
  • Tested with multiple adapters: Same issue occurs with both PostgreSQL and SurrealDB adapters
  • User can sign in normally: Direct email/password authentication works correctly

OAuth Flow Analysis

  1. OpenID Configuration: Properly returns at /.well-known/openid-configuration
  2. Authorization: Successfully authorizes and generates code
  3. Token Exchange: Successfully exchanges code for access token
  4. Userinfo Retrieval: Fails to populate user fields despite valid token

Client Configuration

Frontend apps use the better-auth Svelte client:

import { createAuthClient } from 'better-auth/svelte';
import { jwtClient } from 'better-auth/client/plugins';

export const authClient = createAuthClient({
  plugins: [jwtClient()],
  baseURL: env.PUBLIC_BACKEND_URL
});

Potential Issues

  1. The userinfo endpoint seems to be unable to properly retrieve or map user data from the database
  2. The sub claim is correctly populated but other standard OIDC claims are missing
  3. This might be related to how the OIDC provider plugin fetches or transforms user data for the userinfo response

Workaround Attempts

  • Verified database contains complete user data
  • Tested with different database adapters (PostgreSQL, SurrealDB)
  • Confirmed proper environment variable configuration
  • Validated OAuth client credentials
  • Issue persists across all attempts
  • Auth configuration: apps/backend/src/routes/auth/auth.ts
  • Environment variables properly configured for:
    • OAUTH_CLIENT_ID
    • OAUTH_CLIENT_SECRET
    • BETTER_AUTH_URL
    • OIDC_LOGIN_PAGE_URL
    • Database connection strings

Request for Help

It appears there may be a bug in the OIDC provider plugin where the userinfo endpoint fails to properly retrieve complete user data from the database, even though the user exists with all required fields. The authentication flow completes but returns an incomplete user object, causing the "email_is_missing" error.

Originally created by @JustKira on GitHub (Aug 27, 2025). ## Issue Summary When using better-auth's OIDC provider plugin with the genericOAuth plugin as client, the userinfo endpoint returns incomplete user data (missing email, name, emailVerified fields) even though a valid user exists in the database with complete information. ## Environment - **better-auth version**: 1.3.7 - **Database adapters tested**: - PostgreSQL (using `pg` adapter) - SurrealDB (using custom `better-auth-surrealdb` adapter v1.0.1) - **Runtime**: Bun (latest) - **Framework**: Hono v4.8.4 - **Node environment**: Backend API service ## Project Structure This is a monorepo with the following structure: ``` uni-fied/ ├── apps/ │ ├── accounts/ # SvelteKit auth UI app │ ├── backend/ # Hono + better-auth API server │ ├── platform/ # Platform app │ └── studio/ # Studio app └── packages/ └── api/ # Shared API types ``` ## Configuration ### Backend Auth Setup (`apps/backend/src/routes/auth/auth.ts`) ```typescript import { betterAuth } from "better-auth"; import { admin, genericOAuth, jwt, oidcProvider, openAPI } from "better-auth/plugins"; export const auth = betterAuth({ database: new Pool({ connectionString: env.POSTGRE_URL, }), emailAndPassword: { enabled: true, }, trustedOrigins: env.ALLOWED_ORIGINS.split(","), advanced: { crossSubDomainCookies: { enabled: true, domain: env.TRUSTED_SUBDOMAIN_COOKIES, }, }, plugins: [ admin(), openAPI(), jwt(), // OAuth Client Configuration genericOAuth({ config: [ { providerId: "unified", clientId: env.OAUTH_CLIENT_ID, clientSecret: env.OAUTH_CLIENT_SECRET, discoveryUrl: `${env.BETTER_AUTH_URL}/api/auth/.well-known/openid-configuration`, }, ], }), // OIDC Provider Configuration oidcProvider({ loginPage: env.OIDC_LOGIN_PAGE_URL, useJWTPlugin: true, trustedClients: [ { clientId: env.OAUTH_CLIENT_ID, clientSecret: env.OAUTH_CLIENT_SECRET, disabled: false, metadata: {}, name: "unified", type: "web", skipConsent: true, redirectURLs: [ `${env.BETTER_AUTH_URL}/api/auth/oauth2/callback/unified`, ], }, ], }), ], }); ``` ## Reproduction Steps 1. **User registers via email/password** - User successfully creates account at `/api/auth/sign-up/email` - User data is properly stored in database with complete fields (email, name, etc.) 2. **OAuth2 flow initiates** - Client calls `/api/auth/sign-in/oauth2` - Redirect to authorization endpoint: `/api/auth/oauth2/authorize` - User authenticates successfully - Authorization code is generated 3. **Token exchange succeeds** - Callback to `/api/auth/oauth2/callback/unified` with authorization code - Token endpoint `/api/auth/oauth2/token` successfully returns access token 4. **Userinfo endpoint fails to return complete data** - Request to `/api/auth/oauth2/userinfo` with valid access token - Returns incomplete user object ## Actual Behavior ### Console Output ``` <-- POST /api/auth/sign-in/oauth2 --> POST /api/auth/sign-in/oauth2 200 4s <-- GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=... --> GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=... 302 12ms <-- POST /api/auth/sign-up/email --> POST /api/auth/sign-up/email 200 3s <-- GET /api/auth/oauth2/callback/unified?code=NRINWztknEgYNHg9ikKo9w7gordUlWBD&state=... <-- POST /api/auth/oauth2/token --> POST /api/auth/oauth2/token 200 2s <-- GET /api/auth/oauth2/userinfo --> GET /api/auth/oauth2/userinfo 200 489ms 2025-08-27T23:11:14.251Z ERROR [Better Auth]: Unable to get user info { id: "W0PZ2peuQ4XFjhtJghOPAZ3u6LFLaUSC", emailVerified: undefined, email: undefined, image: undefined, name: undefined, sub: "W0PZ2peuQ4XFjhtJghOPAZ3u6LFLaUSC", } --> GET /api/auth/oauth2/callback/unified?code=... 302 3s <-- GET /api/auth/error?error=email_is_missing ``` ## Expected Behavior The userinfo endpoint should return complete user data including: - `email`: User's email address - `name`: User's name - `emailVerified`: Email verification status - `image`: User's profile image (if available) - `id`/`sub`: User identifier (this works correctly) ## Additional Context ### Database Verification - **Confirmed**: The user exists in the database with all fields populated - **Tested with multiple adapters**: Same issue occurs with both PostgreSQL and SurrealDB adapters - **User can sign in normally**: Direct email/password authentication works correctly ### OAuth Flow Analysis 1. **OpenID Configuration**: Properly returns at `/.well-known/openid-configuration` 2. **Authorization**: Successfully authorizes and generates code 3. **Token Exchange**: Successfully exchanges code for access token 4. **Userinfo Retrieval**: Fails to populate user fields despite valid token ### Client Configuration Frontend apps use the better-auth Svelte client: ```typescript import { createAuthClient } from 'better-auth/svelte'; import { jwtClient } from 'better-auth/client/plugins'; export const authClient = createAuthClient({ plugins: [jwtClient()], baseURL: env.PUBLIC_BACKEND_URL }); ``` ## Potential Issues 1. The userinfo endpoint seems to be unable to properly retrieve or map user data from the database 2. The `sub` claim is correctly populated but other standard OIDC claims are missing 3. This might be related to how the OIDC provider plugin fetches or transforms user data for the userinfo response ## Workaround Attempts - ✅ Verified database contains complete user data - ✅ Tested with different database adapters (PostgreSQL, SurrealDB) - ✅ Confirmed proper environment variable configuration - ✅ Validated OAuth client credentials - ❌ Issue persists across all attempts ## Related Code - Auth configuration: `apps/backend/src/routes/auth/auth.ts` - Environment variables properly configured for: - `OAUTH_CLIENT_ID` - `OAUTH_CLIENT_SECRET` - `BETTER_AUTH_URL` - `OIDC_LOGIN_PAGE_URL` - Database connection strings ## Request for Help It appears there may be a bug in the OIDC provider plugin where the userinfo endpoint fails to properly retrieve complete user data from the database, even though the user exists with all required fields. The authentication flow completes but returns an incomplete user object, causing the "email_is_missing" error.
GiteaMirror added the bug label 2026-03-13 09:04:10 -05:00
Author
Owner

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

The OIDC provider's userinfo endpoint builds its response from the user object returned by your database adapter, mapping fields like email, name, emailVerified, and image based on the scopes granted in the access token. If these fields are missing or undefined, it's usually because the adapter isn't returning them with the expected names, or the schema mapping is off. This can happen if your adapter or schema uses custom field names, or if the output transformation logic isn't mapping fields correctly—even if the database itself has the data [source].

To fix this, double-check your adapter and schema configuration to ensure all standard fields (email, name, emailVerified, image) are mapped and returned as expected. If you're using a custom or external adapter (like SurrealDB), make sure it conforms to the better-auth adapter contract and output mapping [source].

If you need to customize which claims are returned, you can use the getAdditionalUserInfoClaim option in the oidcProvider plugin to explicitly add or transform user fields in the userinfo response [source].

For OAuth client flows, if you see missing fields, you can also override the getUserInfo function in your genericOAuth config to decode the token and map fields directly [source].

In short: review your adapter's output, schema field names, and consider using getAdditionalUserInfoClaim or getUserInfo overrides to ensure all required user fields are present in the userinfo response.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 27, 2025): <!-- Answer --> The OIDC provider's userinfo endpoint builds its response from the user object returned by your database adapter, mapping fields like `email`, `name`, `emailVerified`, and `image` based on the scopes granted in the access token. If these fields are missing or undefined, it's usually because the adapter isn't returning them with the expected names, or the schema mapping is off. This can happen if your adapter or schema uses custom field names, or if the output transformation logic isn't mapping fields correctly—even if the database itself has the data [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/plugins/oidc-provider/index.ts). To fix this, double-check your adapter and schema configuration to ensure all standard fields (`email`, `name`, `emailVerified`, `image`) are mapped and returned as expected. If you're using a custom or external adapter (like SurrealDB), make sure it conforms to the better-auth adapter contract and output mapping [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/adapters/create-adapter/index.ts). If you need to customize which claims are returned, you can use the `getAdditionalUserInfoClaim` option in the `oidcProvider` plugin to explicitly add or transform user fields in the userinfo response [[source]](https://github.com/better-auth/better-auth/pull/3790). For OAuth client flows, if you see missing fields, you can also override the `getUserInfo` function in your `genericOAuth` config to decode the token and map fields directly [[source]](https://github.com/better-auth/better-auth/issues/2154#issuecomment-2782150819). In short: review your adapter's output, schema field names, and consider using `getAdditionalUserInfoClaim` or `getUserInfo` overrides to ensure all required user fields are present in the userinfo response. <!-- 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/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/5fa5c2da-4186-4e96-b3df-bc842bf2ef41?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4275)
Author
Owner

@JustKira commented on GitHub (Aug 27, 2025):

Ok issues was in the Scopes i have added all required scopes but now i get issue of

account_not_linked

import { betterAuth } from "better-auth";
import { env } from "$src/env";
import {
  admin,
  genericOAuth,
  jwt,
  oidcProvider,
  openAPI,
} from "better-auth/plugins";
import { Hono } from "hono";

import { surrealAdapter } from "better-auth-surrealdb";
export const auth = betterAuth({
  database: surrealAdapter({
    endpoint: env.SURREALDB_ENDPOINT,
    database: env.SURREALDB_DATABASE,
    namespace: env.SURREALDB_NAMESPACE,
    auth: {
      password: env.SURREALDB_PASSWORD,
      username: env.SURREALDB_USERNAME,
    },
  }),
  emailAndPassword: {
    enabled: true,
  },
  trustedOrigins: env.ALLOWED_ORIGINS.split(","),
  advanced: {
    crossSubDomainCookies: {
      enabled: true,
      domain: env.TRUSTED_SUBDOMAIN_COOKIES,
    },
  },
  plugins: [
    admin(),
    openAPI(),
    jwt(),
    genericOAuth({
      config: [
        {
          providerId: "unified",
          clientId: env.OAUTH_CLIENT_ID,
          clientSecret: env.OAUTH_CLIENT_SECRET,
          discoveryUrl: `${env.BETTER_AUTH_URL}/api/auth/.well-known/openid-configuration`,
          scopes: ["openid", "email", "profile"],
        },
      ],
    }),
    oidcProvider({
      loginPage: env.OIDC_LOGIN_PAGE_URL,
      useJWTPlugin: true,
      trustedClients: [
        {
          clientId: env.OAUTH_CLIENT_ID,
          clientSecret: env.OAUTH_CLIENT_SECRET,
          disabled: false,
          metadata: {},
          name: "unified",
          type: "web",
          skipConsent: true,
          redirectURLs: [
            `${env.BETTER_AUTH_URL}/api/auth/oauth2/callback/unified`,
          ],
        },
      ],
    }),
  ],

  // socialProviders: {
  //   discord: {
  //     clientId: env.DISCORD_CLIENT_ID,
  //     clientSecret: env.DISCORD_CLIENT_SECRET,
  //     accessType: "offline",
  //     scope: ["identify", "email", "openid", "sdk."],
  //   },
  // },
});

export const AuthRoute = new Hono();

AuthRoute.on(["POST", "GET"], "/*", (c) => {
  return auth.handler(c.req.raw);
});

here updated config
@dosu

@JustKira commented on GitHub (Aug 27, 2025): Ok issues was in the Scopes i have added all required scopes but now i get issue of account_not_linked ```ts import { betterAuth } from "better-auth"; import { env } from "$src/env"; import { admin, genericOAuth, jwt, oidcProvider, openAPI, } from "better-auth/plugins"; import { Hono } from "hono"; import { surrealAdapter } from "better-auth-surrealdb"; export const auth = betterAuth({ database: surrealAdapter({ endpoint: env.SURREALDB_ENDPOINT, database: env.SURREALDB_DATABASE, namespace: env.SURREALDB_NAMESPACE, auth: { password: env.SURREALDB_PASSWORD, username: env.SURREALDB_USERNAME, }, }), emailAndPassword: { enabled: true, }, trustedOrigins: env.ALLOWED_ORIGINS.split(","), advanced: { crossSubDomainCookies: { enabled: true, domain: env.TRUSTED_SUBDOMAIN_COOKIES, }, }, plugins: [ admin(), openAPI(), jwt(), genericOAuth({ config: [ { providerId: "unified", clientId: env.OAUTH_CLIENT_ID, clientSecret: env.OAUTH_CLIENT_SECRET, discoveryUrl: `${env.BETTER_AUTH_URL}/api/auth/.well-known/openid-configuration`, scopes: ["openid", "email", "profile"], }, ], }), oidcProvider({ loginPage: env.OIDC_LOGIN_PAGE_URL, useJWTPlugin: true, trustedClients: [ { clientId: env.OAUTH_CLIENT_ID, clientSecret: env.OAUTH_CLIENT_SECRET, disabled: false, metadata: {}, name: "unified", type: "web", skipConsent: true, redirectURLs: [ `${env.BETTER_AUTH_URL}/api/auth/oauth2/callback/unified`, ], }, ], }), ], // socialProviders: { // discord: { // clientId: env.DISCORD_CLIENT_ID, // clientSecret: env.DISCORD_CLIENT_SECRET, // accessType: "offline", // scope: ["identify", "email", "openid", "sdk."], // }, // }, }); export const AuthRoute = new Hono(); AuthRoute.on(["POST", "GET"], "/*", (c) => { return auth.handler(c.req.raw); }); ``` here updated config @dosu
Author
Owner

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

The account_not_linked error means the OAuth/OIDC flow couldn't match or link the external identity (from your OIDC provider) to a local user account. This usually happens if the account linking logic can't find an existing account for the providerId and accountId (userInfo.id), or can't create a new one due to missing or mismatched identifiers—most often, the email or id fields are missing or not mapped as expected [source].

Here’s what to check:

  • Make sure your SurrealDB adapter implements all required account linking methods and that accountId and userId are always stored and compared as strings. Recent fixes in better-auth addressed bugs where mismatched types (number vs string) caused linking to fail [source].
  • Confirm that the userInfo returned from your OIDC provider always includes a stable, unique id (sub) and a valid email. If either is missing, linking will fail and you’ll see errors like email_is_missing or account_not_linked [source].
  • If you have custom field names or schema, double-check your adapter’s output mapping and the transform logic to ensure all required fields (email, id, providerId, accountId) are mapped and returned as expected [source].
  • If you’re using mapProfileToUser or a custom getUserInfo in your genericOAuth config, make sure these functions always return the required fields as strings.

If you’re still seeing the error, try logging the userInfo object and the account linking queries to confirm what’s being passed and stored. Most often, fixing the field mapping or ensuring string IDs resolves this issue.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 27, 2025): <!-- Answer --> The `account_not_linked` error means the OAuth/OIDC flow couldn't match or link the external identity (from your OIDC provider) to a local user account. This usually happens if the account linking logic can't find an existing account for the providerId and accountId (userInfo.id), or can't create a new one due to missing or mismatched identifiers—most often, the email or id fields are missing or not mapped as expected [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/plugins/generic-oauth/index.ts). Here’s what to check: - Make sure your SurrealDB adapter implements all required account linking methods and that `accountId` and `userId` are always stored and compared as strings. Recent fixes in better-auth addressed bugs where mismatched types (number vs string) caused linking to fail [[source]](https://github.com/better-auth/better-auth/pull/3816). - Confirm that the userInfo returned from your OIDC provider always includes a stable, unique `id` (sub) and a valid `email`. If either is missing, linking will fail and you’ll see errors like `email_is_missing` or `account_not_linked` [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/plugins/generic-oauth/index.ts). - If you have custom field names or schema, double-check your adapter’s output mapping and the transform logic to ensure all required fields (`email`, `id`, `providerId`, `accountId`) are mapped and returned as expected [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/adapters/create-adapter/index.ts). - If you’re using `mapProfileToUser` or a custom `getUserInfo` in your genericOAuth config, make sure these functions always return the required fields as strings. If you’re still seeing the error, try logging the userInfo object and the account linking queries to confirm what’s being passed and stored. Most often, fixing the field mapping or ensuring string IDs resolves this issue. <!-- 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/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1645ce8c-84b9-4ba5-93fb-b8ace69f609f?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4275)
Author
Owner

@JustKira commented on GitHub (Aug 28, 2025):

Ok now i got a new error

ℹ Watching for changes in /Volumes/mypixel/pixelscortex/uni-fied/apps/backend
<-- OPTIONS /api/auth/sign-in/oauth2
--> OPTIONS /api/auth/sign-in/oauth2 204 1ms
<-- POST /api/auth/sign-in/oauth2
<-- GET /api/auth/.well-known/openid-configuration
--> GET /api/auth/.well-known/openid-configuration 200 4ms
--> POST /api/auth/sign-in/oauth2 200 4s
<-- GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified&access_type=offline
--> GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified&access_type=offline 302 7ms
<-- OPTIONS /api/auth/sign-in/email
--> OPTIONS /api/auth/sign-in/email 204 0ms
<-- POST /api/auth/sign-in/email
--> POST /api/auth/sign-in/email 200 2s
<-- GET /api/auth/oauth2/callback/unified?code=z5IJdgWMQbBOigKrSZbXBVhxutTqxsEJ&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx
<-- GET /api/auth/.well-known/openid-configuration
--> GET /api/auth/.well-known/openid-configuration 200 4ms
<-- POST /api/auth/oauth2/token
--> POST /api/auth/oauth2/token 200 2s
--> GET /api/auth/oauth2/callback/unified?code=z5IJdgWMQbBOigKrSZbXBVhxutTqxsEJ&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx 302 4s
<-- GET /api/auth/oauth2/callback/unified?code=ahpvoF4QFtaDkOzyFTdMCPiBqzQQjvHi&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx
2025-08-28T14:02:35.768Z ERROR [Better Auth]: State Mismatch. Verification not found {
  state: "dgls2cxV4FAtwi1-CioUx34Rk4v16vyx",
}
--> GET /api/auth/oauth2/callback/unified?code=ahpvoF4QFtaDkOzyFTdMCPiBqzQQjvHi&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx 302 478ms
<-- GET /api/auth/error?error=please_restart_the_process                                                                       --> GET /api/auth/error?error=please_restart_the_process 200 3ms
<-- GET /favicon.ico
--> GET /favicon.ico 404 1ms
import { betterAuth } from "better-auth";
import { env } from "$src/env";
import {
  admin,
  genericOAuth,
  jwt,
  oidcProvider,
  openAPI,
} from "better-auth/plugins";
import { Hono } from "hono";

import { surrealAdapter } from "better-auth-surrealdb";

export const auth: any = betterAuth({
  database: surrealAdapter({
    endpoint: env.SURREALDB_ENDPOINT,
    database: env.SURREALDB_DATABASE,
    namespace: env.SURREALDB_NAMESPACE,
    auth: {
      password: env.SURREALDB_PASSWORD,
      username: env.SURREALDB_USERNAME,
    },
  }),
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ["email-password", "discord", "unified"],
      allowDifferentEmails: true,
    },
  },
  emailAndPassword: {
    enabled: true,
  },
  trustedOrigins: env.ALLOWED_ORIGINS.split(","),

  plugins: [
    admin(),
    openAPI(),
    jwt(),
    genericOAuth({
      config: [
        {
          providerId: "unified",
          clientId: env.OAUTH_CLIENT_ID,
          clientSecret: env.OAUTH_CLIENT_SECRET,
          discoveryUrl: `${env.BETTER_AUTH_URL}/api/auth/.well-known/openid-configuration`,
          scopes: ["openid", "email", "profile"],
          accessType: "offline",
        },
      ],
    }),
    oidcProvider({
      loginPage: env.OIDC_LOGIN_PAGE_URL,
      useJWTPlugin: true,
      trustedClients: [
        {
          clientId: env.OAUTH_CLIENT_ID,
          clientSecret: env.OAUTH_CLIENT_SECRET,
          disabled: false,
          metadata: {},
          name: "unified",
          type: "web",
          skipConsent: true,
          redirectURLs: [
            `${env.BETTER_AUTH_URL}/api/auth/oauth2/callback/unified`,
          ],
        },
      ],
    }),
  ],

  socialProviders: {
    discord: {
      clientId: env.DISCORD_CLIENT_ID,
      clientSecret: env.DISCORD_CLIENT_SECRET,
      accessType: "offline",
      scope: ["identify", "email", "openid"],
    },
  },
});

export const AuthRoute = new Hono();

AuthRoute.on(["POST", "GET"], "/*", (c) => {
  return auth.handler(c.req.raw);
});

@dosu

@JustKira commented on GitHub (Aug 28, 2025): Ok now i got a new error ``` ℹ Watching for changes in /Volumes/mypixel/pixelscortex/uni-fied/apps/backend <-- OPTIONS /api/auth/sign-in/oauth2 --> OPTIONS /api/auth/sign-in/oauth2 204 1ms <-- POST /api/auth/sign-in/oauth2 <-- GET /api/auth/.well-known/openid-configuration --> GET /api/auth/.well-known/openid-configuration 200 4ms --> POST /api/auth/sign-in/oauth2 200 4s <-- GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified&access_type=offline --> GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified&access_type=offline 302 7ms <-- OPTIONS /api/auth/sign-in/email --> OPTIONS /api/auth/sign-in/email 204 0ms <-- POST /api/auth/sign-in/email --> POST /api/auth/sign-in/email 200 2s <-- GET /api/auth/oauth2/callback/unified?code=z5IJdgWMQbBOigKrSZbXBVhxutTqxsEJ&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx <-- GET /api/auth/.well-known/openid-configuration --> GET /api/auth/.well-known/openid-configuration 200 4ms <-- POST /api/auth/oauth2/token --> POST /api/auth/oauth2/token 200 2s --> GET /api/auth/oauth2/callback/unified?code=z5IJdgWMQbBOigKrSZbXBVhxutTqxsEJ&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx 302 4s <-- GET /api/auth/oauth2/callback/unified?code=ahpvoF4QFtaDkOzyFTdMCPiBqzQQjvHi&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx 2025-08-28T14:02:35.768Z ERROR [Better Auth]: State Mismatch. Verification not found { state: "dgls2cxV4FAtwi1-CioUx34Rk4v16vyx", } --> GET /api/auth/oauth2/callback/unified?code=ahpvoF4QFtaDkOzyFTdMCPiBqzQQjvHi&state=dgls2cxV4FAtwi1-CioUx34Rk4v16vyx 302 478ms <-- GET /api/auth/error?error=please_restart_the_process --> GET /api/auth/error?error=please_restart_the_process 200 3ms <-- GET /favicon.ico --> GET /favicon.ico 404 1ms ``` ```ts import { betterAuth } from "better-auth"; import { env } from "$src/env"; import { admin, genericOAuth, jwt, oidcProvider, openAPI, } from "better-auth/plugins"; import { Hono } from "hono"; import { surrealAdapter } from "better-auth-surrealdb"; export const auth: any = betterAuth({ database: surrealAdapter({ endpoint: env.SURREALDB_ENDPOINT, database: env.SURREALDB_DATABASE, namespace: env.SURREALDB_NAMESPACE, auth: { password: env.SURREALDB_PASSWORD, username: env.SURREALDB_USERNAME, }, }), account: { accountLinking: { enabled: true, trustedProviders: ["email-password", "discord", "unified"], allowDifferentEmails: true, }, }, emailAndPassword: { enabled: true, }, trustedOrigins: env.ALLOWED_ORIGINS.split(","), plugins: [ admin(), openAPI(), jwt(), genericOAuth({ config: [ { providerId: "unified", clientId: env.OAUTH_CLIENT_ID, clientSecret: env.OAUTH_CLIENT_SECRET, discoveryUrl: `${env.BETTER_AUTH_URL}/api/auth/.well-known/openid-configuration`, scopes: ["openid", "email", "profile"], accessType: "offline", }, ], }), oidcProvider({ loginPage: env.OIDC_LOGIN_PAGE_URL, useJWTPlugin: true, trustedClients: [ { clientId: env.OAUTH_CLIENT_ID, clientSecret: env.OAUTH_CLIENT_SECRET, disabled: false, metadata: {}, name: "unified", type: "web", skipConsent: true, redirectURLs: [ `${env.BETTER_AUTH_URL}/api/auth/oauth2/callback/unified`, ], }, ], }), ], socialProviders: { discord: { clientId: env.DISCORD_CLIENT_ID, clientSecret: env.DISCORD_CLIENT_SECRET, accessType: "offline", scope: ["identify", "email", "openid"], }, }, }); export const AuthRoute = new Hono(); AuthRoute.on(["POST", "GET"], "/*", (c) => { return auth.handler(c.req.raw); }); ``` @dosu
Author
Owner

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

The "State Mismatch. Verification not found" error means the OAuth2/OIDC state parameter in your callback couldn't be found or verified in the backend. In better-auth, this state is generated at the start of the flow, stored as a one-time-use record in your database (via the adapter), and deleted after use for security. If the state is missing, expired (10 min window), or not persisted correctly, you'll get this error and a forced restart of the flow [source].

Common causes: backend restarts or hot reloads, DB adapter issues (especially with custom adapters like SurrealDB), state expiry, or multiple callback attempts with the same state. In stateless or serverless setups, or if the adapter doesn't persist verification records reliably, this error is more likely.

To debug: check your SurrealDB for verification/state records being created and deleted as expected, make sure your backend isn't restarting during the flow, and confirm only one callback is happening per auth attempt. Enable debug logging to trace state creation/deletion. If the problem persists, try the official pg adapter in a minimal setup to rule out SurrealDB or Hono/Bun-specific issues [source].

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 28, 2025): <!-- Answer --> The "State Mismatch. Verification not found" error means the OAuth2/OIDC state parameter in your callback couldn't be found or verified in the backend. In better-auth, this state is generated at the start of the flow, stored as a one-time-use record in your database (via the adapter), and deleted after use for security. If the state is missing, expired (10 min window), or not persisted correctly, you'll get this error and a forced restart of the flow [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/oauth2/state.ts). Common causes: backend restarts or hot reloads, DB adapter issues (especially with custom adapters like SurrealDB), state expiry, or multiple callback attempts with the same state. In stateless or serverless setups, or if the adapter doesn't persist verification records reliably, this error is more likely. To debug: check your SurrealDB for verification/state records being created and deleted as expected, make sure your backend isn't restarting during the flow, and confirm only one callback is happening per auth attempt. Enable debug logging to trace state creation/deletion. If the problem persists, try the official pg adapter in a minimal setup to rule out SurrealDB or Hono/Bun-specific issues [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/plugins/generic-oauth/index.ts). <!-- 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/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/e45830b5-1bf7-4862-ba32-4a0d32f5ab42?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4275)
Author
Owner

@JustKira commented on GitHub (Aug 28, 2025):

@dosu i get same issue when using postgress pg pool the issue isn't related to the surrealdb adapter anymore something else is the issue with token verificaiton process i cehcked if tokens are generateed it does

{
	createdAt: d'2025-08-28T14:44:30.565Z',
	expiresAt: d'2025-08-28T14:54:30.565Z',
	id: verification:XmWWDal4GEmXcJabHI38l872RGVowLbN,
	identifier: 'hWuX2zQDdm1Ces61Tm7Y03ckz6Em1hcg',
	updatedAt: d'2025-08-28T14:44:30.565Z',
	value: '{"clientId":"unified_client_id","redirectURI":"http://localhost:3000/api/auth/oauth2/callback/unified","scope":["openid","email","profile"],"userId":"pNB6hXaYganrEiLSarld3PuQ53Tf33dX","authTime":1756392269277,"requireConsent":true,"state":"bLOX7TyjZhpV_HgSPQfo1mnvx6W4hyl3","codeChallengeMethod":"plain"}'
}

here for example the veriifcation in my current database

@JustKira commented on GitHub (Aug 28, 2025): @dosu i get same issue when using postgress pg pool the issue isn't related to the surrealdb adapter anymore something else is the issue with token verificaiton process i cehcked if tokens are generateed it does ```js { createdAt: d'2025-08-28T14:44:30.565Z', expiresAt: d'2025-08-28T14:54:30.565Z', id: verification:XmWWDal4GEmXcJabHI38l872RGVowLbN, identifier: 'hWuX2zQDdm1Ces61Tm7Y03ckz6Em1hcg', updatedAt: d'2025-08-28T14:44:30.565Z', value: '{"clientId":"unified_client_id","redirectURI":"http://localhost:3000/api/auth/oauth2/callback/unified","scope":["openid","email","profile"],"userId":"pNB6hXaYganrEiLSarld3PuQ53Tf33dX","authTime":1756392269277,"requireConsent":true,"state":"bLOX7TyjZhpV_HgSPQfo1mnvx6W4hyl3","codeChallengeMethod":"plain"}' } ``` here for example the veriifcation in my current database
Author
Owner

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

The "State Mismatch. Verification not found" error happens when the OAuth2 state parameter in the callback can't be found or matched in the backend, even though you see the verification record in your database. In better-auth, the state is stored as a one-time-use record (in the verification table, with the identifier field matching the state string) and is deleted after use for security. If the lookup fails, it's usually due to one of these reasons:

  • The state value in the callback doesn't exactly match the identifier in the DB (check for encoding, case, or accidental whitespace).
  • The verification record is expired (10 min window) or was already deleted by a previous callback attempt.
  • There are multiple parallel flows or duplicate callbacks using the same state.
  • There's a schema or mapping mismatch: the adapter expects the identifier field to match the state, but custom schema, pluralization, or field mapping can break this lookup—even with the official pg adapter. This is especially common in monorepo or multi-framework setups where schema drift can occur.
  • The backend is running in multiple instances or restarts during the flow, causing state loss or desync.

To debug:

  1. Log the state value at every step (when generated, stored, and received in the callback) and compare it to the identifier in your verification table.
  2. Check for duplicate or parallel OAuth2 flows that might consume the state early.
  3. Ensure your verification table schema matches the expected structure (id, identifier, value, expiresAt, etc.) and that no custom mapping is interfering.
  4. Enable debug logging in your adapter config to trace all verification queries and deletions.
  5. Double-check that your redirect URIs are an exact match between your OAuth client config and the provider settings—any mismatch can cause state verification to fail, even if the record exists in the DB [source].

If all else fails, try a minimal setup with the official pg adapter and default schema to rule out customizations as the cause. The state mechanism is robust if the schema and mapping are correct, but any misalignment can cause this exact error—even if the record is present in the DB [source].

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 28, 2025): <!-- Answer --> The "State Mismatch. Verification not found" error happens when the OAuth2 state parameter in the callback can't be found or matched in the backend, even though you see the verification record in your database. In better-auth, the state is stored as a one-time-use record (in the verification table, with the identifier field matching the state string) and is deleted after use for security. If the lookup fails, it's usually due to one of these reasons: - The state value in the callback doesn't exactly match the identifier in the DB (check for encoding, case, or accidental whitespace). - The verification record is expired (10 min window) or was already deleted by a previous callback attempt. - There are multiple parallel flows or duplicate callbacks using the same state. - There's a schema or mapping mismatch: the adapter expects the identifier field to match the state, but custom schema, pluralization, or field mapping can break this lookup—even with the official pg adapter. This is especially common in monorepo or multi-framework setups where schema drift can occur. - The backend is running in multiple instances or restarts during the flow, causing state loss or desync. To debug: 1. Log the state value at every step (when generated, stored, and received in the callback) and compare it to the identifier in your verification table. 2. Check for duplicate or parallel OAuth2 flows that might consume the state early. 3. Ensure your verification table schema matches the expected structure (id, identifier, value, expiresAt, etc.) and that no custom mapping is interfering. 4. Enable debug logging in your adapter config to trace all verification queries and deletions. 5. Double-check that your redirect URIs are an exact match between your OAuth client config and the provider settings—any mismatch can cause state verification to fail, even if the record exists in the DB [[source]](https://github.com/better-auth/better-auth/issues/3062). If all else fails, try a minimal setup with the official pg adapter and default schema to rule out customizations as the cause. The state mechanism is robust if the schema and mapping are correct, but any misalignment can cause this exact error—even if the record is present in the DB [[source]](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/oauth2/state.ts). <!-- 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/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ed391fbc-43f2-4a01-a489-e370bd1c179b?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4275)
Author
Owner

@JustKira commented on GitHub (Aug 28, 2025):

@dosu what do u mean by
There's a schema or mapping mismatch: the adapter expects the identifier field to match the state, but custom schema, pluralization, or field mapping can break this lookup—even with the official pg adapter. This is especially common in monorepo or multi-framework setups where schema drift can occur.

also i checked made clean setup is there onther thing im missing like incorrect config

this is the frontend

<script lang="ts">
	import { authClient } from '$lib/clients/auth';
	import { Button } from '$lib/ui/elements/button';

	const signIn = async () => {
		await authClient.signIn.oauth2({
			providerId: 'unified'
		});
	};
</script>

<main class="flex h-screen w-screen items-center justify-center">
	<div class="flex flex-col gap-2">
		<h1>Welcome to Platform</h1>
		<h1 class="text-xs">Login With</h1>
		<Button onclick={() => signIn()}>
	Signin With unified	
		</Button>
	</div>
</main>
@JustKira commented on GitHub (Aug 28, 2025): @dosu what do u mean by There's a schema or mapping mismatch: the adapter expects the identifier field to match the state, but custom schema, pluralization, or field mapping can break this lookup—even with the official pg adapter. This is especially common in monorepo or multi-framework setups where schema drift can occur. also i checked made clean setup is there onther thing im missing like incorrect config this is the frontend ``` <script lang="ts"> import { authClient } from '$lib/clients/auth'; import { Button } from '$lib/ui/elements/button'; const signIn = async () => { await authClient.signIn.oauth2({ providerId: 'unified' }); }; </script> <main class="flex h-screen w-screen items-center justify-center"> <div class="flex flex-col gap-2"> <h1>Welcome to Platform</h1> <h1 class="text-xs">Login With</h1> <Button onclick={() => signIn()}> Signin With unified </Button> </div> </main> ```
Author
Owner

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

A schema or mapping mismatch means the adapter expects your database to have a table/model called verification with a field named identifier that exactly matches the OAuth2 state string during callback. If your schema uses a different table name, field name, or pluralization (like verifications instead of verification), or if you have custom mapping, the lookup can fail—even if the record exists—causing the "State Mismatch. Verification not found" error [source].

This can happen if:

  • Your adapter config uses usePlural: true but your schema/table is singular (or vice versa).
  • You renamed fields or models in your schema but didn't update the adapter mapping.
  • There's schema drift in a monorepo or multi-framework setup, so migrations or code expect different names.

To check: make sure your verification table is named verification (not pluralized unless your adapter expects it), has an identifier field (not renamed), and matches the expected schema: id, identifier, value, expires_at, created_at, updated_at [source]. Also, confirm your adapter config matches your actual DB schema and that no custom mapping or pluralization is interfering [source].

If your setup is clean and you still see this error, enable debug logging to trace the state value at every step, and check for duplicate callbacks or redirect URI mismatches. This will help pinpoint where the lookup fails.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 28, 2025): <!-- Answer --> A schema or mapping mismatch means the adapter expects your database to have a table/model called `verification` with a field named `identifier` that exactly matches the OAuth2 state string during callback. If your schema uses a different table name, field name, or pluralization (like `verifications` instead of `verification`), or if you have custom mapping, the lookup can fail—even if the record exists—causing the "State Mismatch. Verification not found" error [[source](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/oauth2/state.ts)]. This can happen if: - Your adapter config uses `usePlural: true` but your schema/table is singular (or vice versa). - You renamed fields or models in your schema but didn't update the adapter mapping. - There's schema drift in a monorepo or multi-framework setup, so migrations or code expect different names. To check: make sure your verification table is named `verification` (not pluralized unless your adapter expects it), has an `identifier` field (not renamed), and matches the expected schema: `id`, `identifier`, `value`, `expires_at`, `created_at`, `updated_at` [[source](https://github.com/better-auth/better-auth/pull/3906)]. Also, confirm your adapter config matches your actual DB schema and that no custom mapping or pluralization is interfering [[source](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/adapters/create-adapter/index.ts)]. If your setup is clean and you still see this error, enable debug logging to trace the state value at every step, and check for duplicate callbacks or redirect URI mismatches. This will help pinpoint where the lookup fails. <!-- 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/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/c5f63837-efb1-4c66-8574-ff7e31d80c82?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4275)
Author
Owner

@JustKira commented on GitHub (Aug 28, 2025):

<-- GET /api/auth/get-session
--> GET /api/auth/get-session 200 2s
<-- GET /api/auth/get-session
--> GET /api/auth/get-session 200 2ms
<-- POST /api/auth/sign-in/oauth2
<-- GET /api/auth/.well-known/openid-configuration
--> GET /api/auth/.well-known/openid-configuration 200 4ms
--> POST /api/auth/sign-in/oauth2 200 2s
<-- GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified
--> GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified 302 1s
<-- GET /api/auth/oauth2/callback/unified?code=9jXQ5wpCK35EIpCcI6xVr4EcnRB6InWu&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ
<-- GET /api/auth/.well-known/openid-configuration
--> GET /api/auth/.well-known/openid-configuration 200 3ms
<-- POST /api/auth/oauth2/token
--> POST /api/auth/oauth2/token 200 2s
--> GET /api/auth/oauth2/callback/unified?code=9jXQ5wpCK35EIpCcI6xVr4EcnRB6InWu&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ 302 6s
<-- GET /api/auth/oauth2/callback/unified?code=DuINDvgq3L0atNAgJ3y4HlrQN7C9Th4G&state=nAslYWIQLKy6e7E5w1pXy9ElrvY5ixjJ
2025-08-28T15:51:37.629Z ERROR [Better Auth]: State Mismatch. Verification not found {
  state: "nAslYWIQLKy6e7E5w1pXy9ElrvY5ixjJ",
}
--> GET /api/auth/oauth2/callback/unified?code=DuINDvgq3L0atNAgJ3y4HlrQN7C9Th4G&state=nAslYWIQLKy6e7E5w1pXy9ElrvY5ixjJ 302 612ms
<-- GET /api/auth/error?error=please_restart_the_process
--> GET /api/auth/error?error=please_restart_the_process 200 2ms

Well here are some of loggs and i see there
/api/auth/oauth2/callback/unified?code=
getting called twice why is that

does having
backend
auth-ui

and platform that trying to use oauth

casuing issue
do i need to have ui and backend in the same place?

@dosu

@JustKira commented on GitHub (Aug 28, 2025): ``` <-- GET /api/auth/get-session --> GET /api/auth/get-session 200 2s <-- GET /api/auth/get-session --> GET /api/auth/get-session 200 2ms <-- POST /api/auth/sign-in/oauth2 <-- GET /api/auth/.well-known/openid-configuration --> GET /api/auth/.well-known/openid-configuration 200 4ms --> POST /api/auth/sign-in/oauth2 200 2s <-- GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified --> GET /api/auth/oauth2/authorize?response_type=code&client_id=unified_client_id&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ&scope=openid+email+profile&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Foauth2%2Fcallback%2Funified 302 1s <-- GET /api/auth/oauth2/callback/unified?code=9jXQ5wpCK35EIpCcI6xVr4EcnRB6InWu&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ <-- GET /api/auth/.well-known/openid-configuration --> GET /api/auth/.well-known/openid-configuration 200 3ms <-- POST /api/auth/oauth2/token --> POST /api/auth/oauth2/token 200 2s --> GET /api/auth/oauth2/callback/unified?code=9jXQ5wpCK35EIpCcI6xVr4EcnRB6InWu&state=1Yse60Wl20--Q0nXlOi8dyFAUU_hrTzJ 302 6s <-- GET /api/auth/oauth2/callback/unified?code=DuINDvgq3L0atNAgJ3y4HlrQN7C9Th4G&state=nAslYWIQLKy6e7E5w1pXy9ElrvY5ixjJ 2025-08-28T15:51:37.629Z ERROR [Better Auth]: State Mismatch. Verification not found { state: "nAslYWIQLKy6e7E5w1pXy9ElrvY5ixjJ", } --> GET /api/auth/oauth2/callback/unified?code=DuINDvgq3L0atNAgJ3y4HlrQN7C9Th4G&state=nAslYWIQLKy6e7E5w1pXy9ElrvY5ixjJ 302 612ms <-- GET /api/auth/error?error=please_restart_the_process --> GET /api/auth/error?error=please_restart_the_process 200 2ms ``` Well here are some of loggs and i see there /api/auth/oauth2/callback/unified?code= getting called twice why is that does having backend auth-ui and platform that trying to use oauth casuing issue do i need to have ui and backend in the same place? @dosu
Author
Owner

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

The reason you're seeing /api/auth/oauth2/callback/unified called twice—and getting a "State Mismatch. Verification not found" error on the second call—is that better-auth enforces strict one-time use of the OAuth2 state token. After the first successful callback, the state is deleted from the database, so any second call (with the same or a new state) will always fail with this error by design see source.

This double-callback usually happens if your frontend or platform app is triggering the callback endpoint more than once—often due to a misconfigured callbackURL, a redirect loop, or client-side navigation that re-hits the callback route. In multi-app or monorepo setups, this is common if both the UI and platform try to handle the OAuth callback, or if the callbackURL is set to the API endpoint instead of a user-facing route see example.

You do NOT need to colocate your UI and backend, but you must ensure only one app handles the OAuth callback per flow. Best practice: let the backend handle /api/auth/oauth2/callback/unified, then redirect the user to a frontend route (like /dashboard or /welcome). On the frontend, avoid setting callbackURL to the API callback endpoint—set it to a user-facing page instead, and make sure only one app is responsible for handling the OAuth redirect see guidance.

To debug: check your frontend and platform routing for any code that could be navigating to the callback endpoint after the backend already handled it. Use browser devtools/network logs to confirm the sequence. Once you ensure only one callback per flow, the state mismatch error should disappear.

To reply, just mention @dosu.


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

@dosubot[bot] commented on GitHub (Aug 28, 2025): <!-- Answer --> The reason you're seeing /api/auth/oauth2/callback/unified called twice—and getting a "State Mismatch. Verification not found" error on the second call—is that better-auth enforces strict one-time use of the OAuth2 state token. After the first successful callback, the state is deleted from the database, so any second call (with the same or a new state) will always fail with this error by design [see source](https://github.com/better-auth/better-auth/blob/6083855a5377e3e0dbb25b4ce915eaf35ce25ed2/packages/better-auth/src/oauth2/state.ts). This double-callback usually happens if your frontend or platform app is triggering the callback endpoint more than once—often due to a misconfigured callbackURL, a redirect loop, or client-side navigation that re-hits the callback route. In multi-app or monorepo setups, this is common if both the UI and platform try to handle the OAuth callback, or if the callbackURL is set to the API endpoint instead of a user-facing route [see example](https://github.com/better-auth/better-auth/issues/3488). You do NOT need to colocate your UI and backend, but you must ensure only one app handles the OAuth callback per flow. Best practice: let the backend handle /api/auth/oauth2/callback/unified, then redirect the user to a frontend route (like /dashboard or /welcome). On the frontend, avoid setting callbackURL to the API callback endpoint—set it to a user-facing page instead, and make sure only one app is responsible for handling the OAuth redirect [see guidance](https://github.com/better-auth/better-auth/issues/3289). To debug: check your frontend and platform routing for any code that could be navigating to the callback endpoint after the backend already handled it. Use browser devtools/network logs to confirm the sequence. Once you ensure only one callback per flow, the state mismatch error should disappear. <!-- 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/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/e0498aa9-fb61-4a6e-a66b-655f668dfb5d?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4275)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1798