[GH-ISSUE #4998] Discord Provider throwing state_mismatch error every time in latest versions #10136

Closed
opened 2026-04-13 06:04:18 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @nwvh on GitHub (Sep 30, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4998

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Set up a new, or use an existing Better Auth project using the latest version
  2. Setup your Discord provider according to the documentation
  3. Try to log in

Current vs. Expected behavior

Expected behavior: The user should get signed in smoothly
Current behavior: The user gets thrown a state_mismatch error instead

What version of Better Auth are you using?

1.3.24

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Pro",
    "release": "10.0.26100",
    "cpuCount": 16,
    "cpuModel": "AMD Ryzen 7 5800X 8-Core Processor             ",
    "totalMemory": "31.89 GB",
    "freeMemory": "7.27 GB"
  },
  "node": {
    "version": "v21.6.2",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.5.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^15.5.4"
    },
    {
      "name": "react",
      "version": "^19.1.1"
    }
  ],
  "databases": [
    {
      "name": "mysql2",
      "version": "^3.15.1"
    },
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "kysely",
      "version": "^0.27.6"
    }
  ],
  "betterAuth": {
    "version": "1.3.0",
    "config": {
      "database": {
        "dialect": {},
        "type": "mysql"
      },
      "telemetry": {
        "enabled": false
      },
      "socialProviders": {
        "discord": {
          "overrideUserInfoOnSignIn": true,
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]",
          "redirectURI": "http://localhost:3000/api/auth/callback/discord"
        }
      },
      "session": {
        "expiresIn": 604800,
        "updateAge": 86400,
        "cookieCache": {
          "enabled": true,
          "maxAge": 300
        }
      },
      "user": {
        "additionalFields": {
          "uuid": {
            "type": "number",
            "defaultValue": 0
          },
          "reseller": {
            "type": "boolean",
            "defaultValue": 0
          },
          "role": {
            "type": "string",
            "defaultValue": "user"
          },
          "beta": {
            "type": "boolean",
            "defaultValue": 0
          },
          "blacklist": {
            "type": "boolean",
            "defaultValue": 0
          },
          "blacklistReason": {
            "type": "string",
            "nullable": true,
            "defaultValue": null
          },
          "blacklistedAt": {
            "type": "date",
            "nullable": true,
            "defaultValue": null
          },
          "blacklistedBy": {
            "type": "string",
            "nullable": true,
            "defaultValue": null
          },
          "blacklistId": {
            "type": "string",
            "nullable": true,
            "defaultValue": null
          }
        }
      },
      "plugins": [
        {
          "name": "api-key",
          "config": {
            "id": "api-key",
            "$ERROR_CODES": {
              "INVALID_METADATA_TYPE": "metadata must be an object or undefined",
              "REFILL_AMOUNT_AND_INTERVAL_REQUIRED": "refillAmount is required when refillInterval is provided",
              "REFILL_INTERVAL_AND_AMOUNT_REQUIRED": "refillInterval is required when refillAmount is provided",
              "USER_BANNED": "User is banned",
              "UNAUTHORIZED_SESSION": "Unauthorized or invalid session",
              "KEY_NOT_FOUND": "API Key not found",
              "KEY_DISABLED": "API Key is disabled",
              "KEY_EXPIRED": "API Key has expired",
              "USAGE_EXCEEDED": "API Key has reached its usage limit",
              "KEY_NOT_RECOVERABLE": "API Key is not recoverable",
              "EXPIRES_IN_IS_TOO_SMALL": "The expiresIn is smaller than the predefined minimum value.",
              "EXPIRES_IN_IS_TOO_LARGE": "The expiresIn is larger than the predefined maximum value.",
              "INVALID_REMAINING": "The remaining count is either too large or too small.",
              "INVALID_PREFIX_LENGTH": "The prefix length is either too large or too small.",
              "INVALID_NAME_LENGTH": "The name length is either too large or too small.",
              "METADATA_DISABLED": "Metadata is disabled.",
              "RATE_LIMIT_EXCEEDED": "Rate limit exceeded.",
              "NO_VALUES_TO_UPDATE": "No values to update.",
              "KEY_DISABLED_EXPIRATION": "Custom key expiration values are disabled.",
              "INVALID_API_KEY": "Invalid API key.",
              "INVALID_USER_ID_FROM_API_KEY": "The user id from the API key is invalid.",
              "INVALID_API_KEY_GETTER_RETURN_TYPE": "API Key getter returned an invalid key type. Expected string.",
              "SERVER_ONLY_PROPERTY": "The property you're trying to set can only be set from the server auth instance only.",
              "FAILED_TO_UPDATE_API_KEY": "Failed to update API key",
              "NAME_REQUIRED": "API Key name is required."
            },
            "hooks": {
              "before": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "apikey": {
                "fields": {
                  "name": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "start": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "prefix": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "key": {
                    "type": "string",
                    "required": true,
                    "input": false
                  },
                  "userId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id"
                    },
                    "required": true,
                    "input": false
                  },
                  "refillInterval": {
                    "type": "number",
                    "required": false,
                    "input": false
                  },
                  "refillAmount": {
                    "type": "number",
                    "required": false,
                    "input": false
                  },
                  "lastRefillAt": {
                    "type": "date",
                    "required": false,
                    "input": false
                  },
                  "enabled": {
                    "type": "boolean",
                    "required": false,
                    "input": false,
                    "defaultValue": true
                  },
                  "rateLimitEnabled": {
                    "type": "boolean",
                    "required": false,
                    "input": false,
                    "defaultValue": true
                  },
                  "rateLimitTimeWindow": {
                    "type": "number",
                    "required": false,
                    "input": false,
                    "defaultValue": 86400000
                  },
                  "rateLimitMax": {
                    "type": "number",
                    "required": false,
                    "input": false,
                    "defaultValue": 10
                  },
                  "requestCount": {
                    "type": "number",
                    "required": false,
                    "input": false,
                    "defaultValue": 0
                  },
                  "remaining": {
                    "type": "number",
                    "required": false,
                    "input": false
                  },
                  "lastRequest": {
                    "type": "date",
                    "required": false,
                    "input": false
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": false,
                    "input": false
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true,
                    "input": false
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": true,
                    "input": false
                  },
                  "permissions": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "metadata": {
                    "type": "string",
                    "required": false,
                    "input": true,
                    "transform": {}
                  }
                }
              }
            }
          }
        }
      ],
      "advanced": {
        "cookiePrefix": "wx"
      }
    }
  }
}

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

Backend

Auth config (if applicable)

export const auth = betterAuth({
	database: { dialect, type: "mysql" },
	telemetry: { enabled: false },
	socialProviders: {
		discord: {
			overrideUserInfoOnSignIn: true,
			clientId: process.env.DISCORD_CLIENT_ID as string,
			clientSecret: process.env.DISCORD_CLIENT_SECRET as string,
			redirectURI: `${process.env.BETTER_AUTH_URL}/api/auth/callback/discord`,
			mapProfileToUser: async (profile) => {
				return {
					name: profile.global_name?.toString() || profile.username.toString(),
					email: profile.email,
					emailVerified: profile.verified,
					image: profile.image_url,
				};
			},
		},
	},
	session: {
		expiresIn: 7 * 24 * 60 * 60, // 7 days
		updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
		cookieCache: {
			enabled: true,
			maxAge: 5 * 60, // Cache duration in seconds
		},
	},
	user: {
		additionalFields: {
			uuid: {
				type: "number",
				defaultValue: 0,
			},
			reseller: {
				type: "boolean",
				defaultValue: 0,
			},
			role: {
				type: "string",
				defaultValue: "user",
			},
			beta: {
				type: "boolean",
				defaultValue: 0,
			},
			blacklist: {
				type: "boolean",
				defaultValue: 0,
			},
			blacklistReason: {
				type: "string",
				nullable: true,
				defaultValue: null,
			},
			blacklistedAt: {
				type: "date",
				nullable: true,
				defaultValue: null,
			},
			blacklistedBy: {
				type: "string",
				nullable: true,
				defaultValue: null,
			},
			blacklistId: {
				type: "string",
				nullable: true,
				defaultValue: null,
			},
		},
	},
	plugins: [
		apiKey({
			enableMetadata: true,
			customKeyGenerator: (options: {
				length: number;
				prefix: string | undefined;
			}) => {
				const key = crypto.randomUUID();
				return `${options.prefix}${key}`;
			},
		}),
	],
	advanced: {
		cookiePrefix: "wx",
	},
});

Additional context

Downgrading to version 1.3.0 fixes this issue, haven't tried more recent versions that that though.

My Callback URLs are matching both in the Discord Dev Portal and in my Auth configuration

Image
Originally created by @nwvh on GitHub (Sep 30, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4998 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Set up a new, or use an existing Better Auth project using the latest version 2. Setup your Discord provider according to the documentation 3. Try to log in ### Current vs. Expected behavior Expected behavior: **The user should get signed in smoothly** Current behavior: **The user gets thrown a `state_mismatch` error instead** ### What version of Better Auth are you using? 1.3.24 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Pro", "release": "10.0.26100", "cpuCount": 16, "cpuModel": "AMD Ryzen 7 5800X 8-Core Processor ", "totalMemory": "31.89 GB", "freeMemory": "7.27 GB" }, "node": { "version": "v21.6.2", "env": "development" }, "packageManager": { "name": "npm", "version": "10.5.0" }, "frameworks": [ { "name": "next", "version": "^15.5.4" }, { "name": "react", "version": "^19.1.1" } ], "databases": [ { "name": "mysql2", "version": "^3.15.1" }, { "name": "pg", "version": "^8.16.3" }, { "name": "kysely", "version": "^0.27.6" } ], "betterAuth": { "version": "1.3.0", "config": { "database": { "dialect": {}, "type": "mysql" }, "telemetry": { "enabled": false }, "socialProviders": { "discord": { "overrideUserInfoOnSignIn": true, "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "redirectURI": "http://localhost:3000/api/auth/callback/discord" } }, "session": { "expiresIn": 604800, "updateAge": 86400, "cookieCache": { "enabled": true, "maxAge": 300 } }, "user": { "additionalFields": { "uuid": { "type": "number", "defaultValue": 0 }, "reseller": { "type": "boolean", "defaultValue": 0 }, "role": { "type": "string", "defaultValue": "user" }, "beta": { "type": "boolean", "defaultValue": 0 }, "blacklist": { "type": "boolean", "defaultValue": 0 }, "blacklistReason": { "type": "string", "nullable": true, "defaultValue": null }, "blacklistedAt": { "type": "date", "nullable": true, "defaultValue": null }, "blacklistedBy": { "type": "string", "nullable": true, "defaultValue": null }, "blacklistId": { "type": "string", "nullable": true, "defaultValue": null } } }, "plugins": [ { "name": "api-key", "config": { "id": "api-key", "$ERROR_CODES": { "INVALID_METADATA_TYPE": "metadata must be an object or undefined", "REFILL_AMOUNT_AND_INTERVAL_REQUIRED": "refillAmount is required when refillInterval is provided", "REFILL_INTERVAL_AND_AMOUNT_REQUIRED": "refillInterval is required when refillAmount is provided", "USER_BANNED": "User is banned", "UNAUTHORIZED_SESSION": "Unauthorized or invalid session", "KEY_NOT_FOUND": "API Key not found", "KEY_DISABLED": "API Key is disabled", "KEY_EXPIRED": "API Key has expired", "USAGE_EXCEEDED": "API Key has reached its usage limit", "KEY_NOT_RECOVERABLE": "API Key is not recoverable", "EXPIRES_IN_IS_TOO_SMALL": "The expiresIn is smaller than the predefined minimum value.", "EXPIRES_IN_IS_TOO_LARGE": "The expiresIn is larger than the predefined maximum value.", "INVALID_REMAINING": "The remaining count is either too large or too small.", "INVALID_PREFIX_LENGTH": "The prefix length is either too large or too small.", "INVALID_NAME_LENGTH": "The name length is either too large or too small.", "METADATA_DISABLED": "Metadata is disabled.", "RATE_LIMIT_EXCEEDED": "Rate limit exceeded.", "NO_VALUES_TO_UPDATE": "No values to update.", "KEY_DISABLED_EXPIRATION": "Custom key expiration values are disabled.", "INVALID_API_KEY": "Invalid API key.", "INVALID_USER_ID_FROM_API_KEY": "The user id from the API key is invalid.", "INVALID_API_KEY_GETTER_RETURN_TYPE": "API Key getter returned an invalid key type. Expected string.", "SERVER_ONLY_PROPERTY": "The property you're trying to set can only be set from the server auth instance only.", "FAILED_TO_UPDATE_API_KEY": "Failed to update API key", "NAME_REQUIRED": "API Key name is required." }, "hooks": { "before": [ {} ] }, "endpoints": {}, "schema": { "apikey": { "fields": { "name": { "type": "string", "required": false, "input": false }, "start": { "type": "string", "required": false, "input": false }, "prefix": { "type": "string", "required": false, "input": false }, "key": { "type": "string", "required": true, "input": false }, "userId": { "type": "string", "references": { "model": "user", "field": "id" }, "required": true, "input": false }, "refillInterval": { "type": "number", "required": false, "input": false }, "refillAmount": { "type": "number", "required": false, "input": false }, "lastRefillAt": { "type": "date", "required": false, "input": false }, "enabled": { "type": "boolean", "required": false, "input": false, "defaultValue": true }, "rateLimitEnabled": { "type": "boolean", "required": false, "input": false, "defaultValue": true }, "rateLimitTimeWindow": { "type": "number", "required": false, "input": false, "defaultValue": 86400000 }, "rateLimitMax": { "type": "number", "required": false, "input": false, "defaultValue": 10 }, "requestCount": { "type": "number", "required": false, "input": false, "defaultValue": 0 }, "remaining": { "type": "number", "required": false, "input": false }, "lastRequest": { "type": "date", "required": false, "input": false }, "expiresAt": { "type": "date", "required": false, "input": false }, "createdAt": { "type": "date", "required": true, "input": false }, "updatedAt": { "type": "date", "required": true, "input": false }, "permissions": { "type": "string", "required": false, "input": false }, "metadata": { "type": "string", "required": false, "input": true, "transform": {} } } } } } } ], "advanced": { "cookiePrefix": "wx" } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: { dialect, type: "mysql" }, telemetry: { enabled: false }, socialProviders: { discord: { overrideUserInfoOnSignIn: true, clientId: process.env.DISCORD_CLIENT_ID as string, clientSecret: process.env.DISCORD_CLIENT_SECRET as string, redirectURI: `${process.env.BETTER_AUTH_URL}/api/auth/callback/discord`, mapProfileToUser: async (profile) => { return { name: profile.global_name?.toString() || profile.username.toString(), email: profile.email, emailVerified: profile.verified, image: profile.image_url, }; }, }, }, session: { expiresIn: 7 * 24 * 60 * 60, // 7 days updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated) cookieCache: { enabled: true, maxAge: 5 * 60, // Cache duration in seconds }, }, user: { additionalFields: { uuid: { type: "number", defaultValue: 0, }, reseller: { type: "boolean", defaultValue: 0, }, role: { type: "string", defaultValue: "user", }, beta: { type: "boolean", defaultValue: 0, }, blacklist: { type: "boolean", defaultValue: 0, }, blacklistReason: { type: "string", nullable: true, defaultValue: null, }, blacklistedAt: { type: "date", nullable: true, defaultValue: null, }, blacklistedBy: { type: "string", nullable: true, defaultValue: null, }, blacklistId: { type: "string", nullable: true, defaultValue: null, }, }, }, plugins: [ apiKey({ enableMetadata: true, customKeyGenerator: (options: { length: number; prefix: string | undefined; }) => { const key = crypto.randomUUID(); return `${options.prefix}${key}`; }, }), ], advanced: { cookiePrefix: "wx", }, }); ``` ### Additional context Downgrading to version `1.3.0` fixes this issue, haven't tried more recent versions that that though. My Callback URLs are matching both in the Discord Dev Portal and in my Auth configuration <img width="480" height="446" alt="Image" src="https://github.com/user-attachments/assets/ae412363-2a29-4c53-94f2-f0d30fede7bc" />
GiteaMirror added the locked label 2026-04-13 06:04:18 -05:00
Author
Owner

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

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

This state_mismatch error in Better Auth v1.3.24 is a known issue caused by changes in how OAuth state and cookies are handled starting in v1.3.23. The framework now requires the OAuth state cookie to be set on the client side, and older workarounds are no longer supported. The recommended fix is to use the authClient directly for social sign-in flows, rather than calling the API endpoint manually or using custom logic. This ensures the state parameter and cookies are managed correctly by Better Auth's client library [source].

If you are using signIn.social from the client, make sure you do not set the callbackURL to the API callback endpoint (e.g., /api/auth/callback/discord). Instead, either omit the callbackURL or set it to a user-facing route. The Discord OAuth app's redirect URI should still point to your API callback endpoint [source].

No breaking changes to Discord provider state/cookie handling were found in recent PRs, but the session and cookie logic is tightly coupled to the client-side flow. If you are triggering the OAuth flow from the backend or a custom endpoint, switch to using the official client-side method.

If you need a migration path from 1.3.0, update your sign-in logic to use the documented client approach and avoid custom state/cookie handling. Let me know if you need a code example for your specific frontend setup.

To reply, just mention @dosu.


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

<!-- gh-comment-id:3353589657 --> @dosubot[bot] commented on GitHub (Sep 30, 2025): <!-- Greeting --> Hi @nwvh! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This `state_mismatch` error in Better Auth v1.3.24 is a known issue caused by changes in how OAuth state and cookies are handled starting in v1.3.23. The framework now requires the OAuth state cookie to be set on the client side, and older workarounds are no longer supported. The recommended fix is to use the `authClient` directly for social sign-in flows, rather than calling the API endpoint manually or using custom logic. This ensures the state parameter and cookies are managed correctly by Better Auth's client library [[source]](https://github.com/better-auth/better-auth/issues/4969). If you are using `signIn.social` from the client, make sure you do not set the `callbackURL` to the API callback endpoint (e.g., `/api/auth/callback/discord`). Instead, either omit the `callbackURL` or set it to a user-facing route. The Discord OAuth app's redirect URI should still point to your API callback endpoint [[source]](https://github.com/better-auth/better-auth/issues/3488). No breaking changes to Discord provider state/cookie handling were found in recent PRs, but the session and cookie logic is tightly coupled to the client-side flow. If you are triggering the OAuth flow from the backend or a custom endpoint, switch to using the official client-side method. If you need a migration path from 1.3.0, update your sign-in logic to use the documented client approach and avoid custom state/cookie handling. Let me know if you need a code example for your specific frontend setup. <!-- 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/1132bb37-58a6-40fd-805f-dffe65da9aa6?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/1132bb37-58a6-40fd-805f-dffe65da9aa6?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/1132bb37-58a6-40fd-805f-dffe65da9aa6?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/1132bb37-58a6-40fd-805f-dffe65da9aa6?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/1132bb37-58a6-40fd-805f-dffe65da9aa6?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/1132bb37-58a6-40fd-805f-dffe65da9aa6?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/1132bb37-58a6-40fd-805f-dffe65da9aa6?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/4998)
Author
Owner

@nwvh commented on GitHub (Oct 1, 2025):

Apparently the issue is that Better Auth doesn't like when I trigger the signIn() on the server through my server action...

<!-- gh-comment-id:3357286816 --> @nwvh commented on GitHub (Oct 1, 2025): Apparently the issue is that Better Auth doesn't like when I trigger the `signIn()` on the server through my server action...
Author
Owner

@david-yan-1 commented on GitHub (Oct 6, 2025):

Apparently the issue is that Better Auth doesn't like when I trigger the signIn() on the server through my server action...

Did you manage to solve this?

<!-- gh-comment-id:3371869831 --> @david-yan-1 commented on GitHub (Oct 6, 2025): > Apparently the issue is that Better Auth doesn't like when I trigger the `signIn()` on the server through my server action... Did you manage to solve this?
Author
Owner

@jacogasp commented on GitHub (Oct 10, 2025):

Apparently the issue is that Better Auth doesn't like when I trigger the signIn() on the server through my server action...

Did you manage to solve this?

Hello, I resolved the server side login with

import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";

export const auth = betterAuth({
    //...your config
    plugins: [nextCookies()] // make sure this is the last plugin in the array
})

Source: https://www.better-auth.com/docs/integrations/next#server-action-cookies

<!-- gh-comment-id:3389138589 --> @jacogasp commented on GitHub (Oct 10, 2025): > > Apparently the issue is that Better Auth doesn't like when I trigger the `signIn()` on the server through my server action... > > Did you manage to solve this? Hello, I resolved the server side login with ``` import { betterAuth } from "better-auth"; import { nextCookies } from "better-auth/next-js"; export const auth = betterAuth({ //...your config plugins: [nextCookies()] // make sure this is the last plugin in the array }) ``` Source: https://www.better-auth.com/docs/integrations/next#server-action-cookies
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10136