state_mismatch with Google sign in on Svelte #2177

Closed
opened 2026-03-13 09:32:06 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @lorizz on GitHub (Oct 22, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Access my /login page
  2. Press "Login with Google"
  3. Complete the login form
  4. Better Auth Error callback with state_mismatch happens
Image

Current vs. Expected behavior

By using the signIn.social on Login with Google button, passing the correct and and credentials is correctly configured on GCP (it's even in prod mode), this error happens, I tried also with auth.api.signInSocial directly but nothing works.
emailAndPassword login/register works without problems.
This happens on production website and in local.

My auth config:

import { betterAuth } from 'better-auth';
import { pool } from '$lib/server/db';
import { env } from '$env/dynamic/private';
import type { Session, User as BetterAuthUser } from 'better-auth';
import type { User as AppUser } from '$lib/types';
import { Resend } from 'resend';
import {
	generateVerificationEmail,
	generatePasswordResetEmail
} from '$lib/email-templates/generator';
import type { SupportedLocale } from '$lib/i18n';
import { emailTranslations } from '$lib/email-templates/translations';

let resend: Resend;

function getResend() {
	if (!resend) {
		resend = new Resend(env.RESEND_API_KEY);
	}
	return resend;
}

const EMAIL_CONFIG = {
	from: 'onboarding@resend.dev',
	appName: 'SoWarmly',
	logoUrl: 'https://tuodominio.com/logo.png',
	accentColor: '#0066ff'
};

function getLocaleFromHeaders(headers?: Headers): SupportedLocale {
	if (!headers) return 'en';

	const acceptLanguage = headers.get('accept-language');
	if (acceptLanguage) {
		const primaryLang = acceptLanguage.split(',')[0].split('-')[0].toLowerCase();
		if (['en', 'it', 'de', 'es', 'fr', 'pt'].includes(primaryLang)) {
			return primaryLang as SupportedLocale;
		}
	}
	return 'en';
}

export const auth = betterAuth({
	secret: env.AUTH_SECRET,
	baseURL: env.BETTER_AUTH_URL,
	basePath: '/auth',
	database: pool,

	logger: {
		level: 'debug',
		disabled: false
	},

	trustedOrigins: ['http://localhost:5173', 'https://sowarmly.com'],
	advanced: {
		useSecureCookies: env.NODE_ENV === 'production',
		cookiePrefix: 'sowarmyly_auth_'
	},

	emailVerification: {
		sendVerificationEmail: async ({ user, url }, request) => {
			const locale = getLocaleFromHeaders(request?.headers);

			const html = generateVerificationEmail({
				name: user.name,
				url,
				locale,
				appName: EMAIL_CONFIG.appName,
				logoUrl: EMAIL_CONFIG.logoUrl,
				accentColor: EMAIL_CONFIG.accentColor
			});

			const { error } = await getResend().emails.send({
				from: EMAIL_CONFIG.from,
				to: user.email,
				subject: emailTranslations[locale].verification.subject,
				html
			});

			if (error) {
				console.error('Failed to send verification email:', error);
				throw new Error('Failed to send verification email');
			}
		}
	},

	emailAndPassword: {
		enabled: true,
		requireEmailVerification: true,
		sendResetPassword: async ({ user, url }, request) => {
			const locale = getLocaleFromHeaders(request?.headers);

			const html = generatePasswordResetEmail({
				name: user.name,
				url,
				locale,
				appName: EMAIL_CONFIG.appName,
				logoUrl: EMAIL_CONFIG.logoUrl,
				accentColor: EMAIL_CONFIG.accentColor
			});

			const { error } = await getResend().emails.send({
				from: EMAIL_CONFIG.from,
				to: user.email,
				subject: emailTranslations[locale].passwordReset.subject,
				html
			});

			if (error) {
				console.error('Failed to send password reset email:', error);
				throw new Error('Failed to send password reset email');
			}
		}
	},

	socialProviders: {
		google: {
			clientId: env.GOOGLE_CLIENT_ID || '',
			clientSecret: env.GOOGLE_CLIENT_SECRET || '',
			enabled: true
		}
	},

	session: {
		cookieCache: {
			enabled: true,
			maxAge: 60 * 60 * 24 * 7
		}
	},

	callbacks: {
		session: async ({ session, user }: { session: Session; user: BetterAuthUser }) => {
			if (session && user) {
				const appUser = user as unknown as AppUser;
				return { ...session, user: appUser };
			}
			return session;
		}
	}
});

my login +page.server.ts (the form is passing an action="?/login" with use:enhance)

import { fail, redirect, isRedirect } from '@sveltejs/kit';
import { z } from 'zod';
import { auth } from '$lib/server/auth';
import type { Actions } from './$types';
import { authClient } from '$lib/client/auth-client';

const LoginSchema = z.object({
	email: z.email({ message: 'Please enter a valid email address.' }),
	password: z.string().min(1, 'Password is required.')
});

export const actions: Actions = {
	email: async ({ request, fetch }) => {
		const formData = await request.formData();
		const body = Object.fromEntries(formData);
		const result = LoginSchema.safeParse(body);

		if (!result.success) {
			return fail(400, { data: body, errors: result.error.flatten().fieldErrors });
		}

		const { email, password } = result.data;

		try {
			const response = await fetch('/auth/sign-in/email', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ email, password })
			});

			if (response.ok) {
				throw redirect(303, '/dashboard');
			} else {
				const errorData = await response.json().catch(() => ({}));
				console.error('Login failed:', errorData);

				if (errorData.message === 'User not verified') {
					throw redirect(303, `/auth/check-email?email=${email}&unverified=true`);
				}

				return fail(401, {
					data: body,
					errors: { _general: ['Invalid email or password. Please try again.'] }
				});
			}
		} catch (err) {
			if (isRedirect(err)) {
				throw err;
			}
			console.error('Login Error:', err);
			return fail(500, { data: body, message: 'An internal server error occurred.' });
		}
	},

	google: async ({ request, url }) => {
		try {
			const response = await authClient.signIn.social({
				provider: 'google',
				callbackURL: '/',
				errorCallbackURL: '/',
				newUserCallbackURL: '/'
			});

			if (response.data && response.data.url) {
				throw redirect(303, response.data.url);
			}
		} catch (err) {
			if (isRedirect(err)) {
				throw err;
			}
			console.error('Google sign-in error:', err);
			return fail(500, { message: 'Could not sign in with Google.' });
		}
	}
};

My authClient:

import { env } from '$env/dynamic/private';
import { createAuthClient } from 'better-auth/svelte';

export const authClient = createAuthClient({
	baseURL: env.BETTER_AUTH_URL,
	basePath: '/auth'
});

Authorized redirect on GCP are correctly configured under "/auth/callback/google" (since I am using basePath "auth")

What version of Better Auth are you using?

1.3.28

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103",
    "release": "24.6.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "8.00 GB",
    "freeMemory": "0.16 GB"
  },
  "node": {
    "version": "v22.17.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.9.2"
  },
  "frameworks": [
    {
      "name": "svelte",
      "version": "^5.0.0"
    },
    {
      "name": "@sveltejs/kit",
      "version": "^2.22.0"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "@neondatabase/serverless",
      "version": "^1.0.2"
    }
  ],
  "betterAuth": {
    "version": "1.3.28",
    "config": {
      "secret": "[REDACTED]",
      "baseURL": "http://localhost:5173",
      "basePath": "/auth",
      "database": {
        "_events": {},
        "_eventsCount": 0,
        "options": {
          "connectionString": "[REDACTED]",
          "max": 20,
          "idleTimeoutMillis": 30000,
          "connectionTimeoutMillis": 10000,
          "min": 0,
          "maxUses": null,
          "allowExitOnIdle": false,
          "maxLifetimeSeconds": 0
        },
        "_clients": [],
        "_idle": [],
        "_expired": {},
        "_pendingQueue": [],
        "ending": false,
        "ended": false
      },
      "logger": {
        "level": "debug",
        "disabled": false
      },
      "trustedOrigins": [
        "http://localhost:5173",
        "https://sowarmly.com"
      ],
      "advanced": {
        "useSecureCookies": false,
        "cookiePrefix": "sowarmyly_auth_"
      },
      "emailVerification": {},
      "emailAndPassword": {
        "enabled": true,
        "requireEmailVerification": true
      },
      "socialProviders": {
        "google": {
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]",
          "enabled": true
        }
      },
      "session": {
        "cookieCache": {
          "enabled": true,
          "maxAge": 604800
        }
      },
      "callbacks": {}
    }
  }
}

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

Client

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

I am using Arc but I tried with Chrome and Safari

Originally created by @lorizz on GitHub (Oct 22, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Access my /login page 2. Press "Login with Google" 3. Complete the login form 4. Better Auth Error callback with state_mismatch happens <img width="1006" height="715" alt="Image" src="https://github.com/user-attachments/assets/ade8a86d-ad52-4066-8a60-04a621656546" /> ### Current vs. Expected behavior By using the signIn.social on Login with Google button, passing the correct and and credentials is correctly configured on GCP (it's even in prod mode), this error happens, I tried also with auth.api.signInSocial directly but nothing works. emailAndPassword login/register works without problems. This happens on production website and in local. My auth config: ```ts import { betterAuth } from 'better-auth'; import { pool } from '$lib/server/db'; import { env } from '$env/dynamic/private'; import type { Session, User as BetterAuthUser } from 'better-auth'; import type { User as AppUser } from '$lib/types'; import { Resend } from 'resend'; import { generateVerificationEmail, generatePasswordResetEmail } from '$lib/email-templates/generator'; import type { SupportedLocale } from '$lib/i18n'; import { emailTranslations } from '$lib/email-templates/translations'; let resend: Resend; function getResend() { if (!resend) { resend = new Resend(env.RESEND_API_KEY); } return resend; } const EMAIL_CONFIG = { from: 'onboarding@resend.dev', appName: 'SoWarmly', logoUrl: 'https://tuodominio.com/logo.png', accentColor: '#0066ff' }; function getLocaleFromHeaders(headers?: Headers): SupportedLocale { if (!headers) return 'en'; const acceptLanguage = headers.get('accept-language'); if (acceptLanguage) { const primaryLang = acceptLanguage.split(',')[0].split('-')[0].toLowerCase(); if (['en', 'it', 'de', 'es', 'fr', 'pt'].includes(primaryLang)) { return primaryLang as SupportedLocale; } } return 'en'; } export const auth = betterAuth({ secret: env.AUTH_SECRET, baseURL: env.BETTER_AUTH_URL, basePath: '/auth', database: pool, logger: { level: 'debug', disabled: false }, trustedOrigins: ['http://localhost:5173', 'https://sowarmly.com'], advanced: { useSecureCookies: env.NODE_ENV === 'production', cookiePrefix: 'sowarmyly_auth_' }, emailVerification: { sendVerificationEmail: async ({ user, url }, request) => { const locale = getLocaleFromHeaders(request?.headers); const html = generateVerificationEmail({ name: user.name, url, locale, appName: EMAIL_CONFIG.appName, logoUrl: EMAIL_CONFIG.logoUrl, accentColor: EMAIL_CONFIG.accentColor }); const { error } = await getResend().emails.send({ from: EMAIL_CONFIG.from, to: user.email, subject: emailTranslations[locale].verification.subject, html }); if (error) { console.error('Failed to send verification email:', error); throw new Error('Failed to send verification email'); } } }, emailAndPassword: { enabled: true, requireEmailVerification: true, sendResetPassword: async ({ user, url }, request) => { const locale = getLocaleFromHeaders(request?.headers); const html = generatePasswordResetEmail({ name: user.name, url, locale, appName: EMAIL_CONFIG.appName, logoUrl: EMAIL_CONFIG.logoUrl, accentColor: EMAIL_CONFIG.accentColor }); const { error } = await getResend().emails.send({ from: EMAIL_CONFIG.from, to: user.email, subject: emailTranslations[locale].passwordReset.subject, html }); if (error) { console.error('Failed to send password reset email:', error); throw new Error('Failed to send password reset email'); } } }, socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID || '', clientSecret: env.GOOGLE_CLIENT_SECRET || '', enabled: true } }, session: { cookieCache: { enabled: true, maxAge: 60 * 60 * 24 * 7 } }, callbacks: { session: async ({ session, user }: { session: Session; user: BetterAuthUser }) => { if (session && user) { const appUser = user as unknown as AppUser; return { ...session, user: appUser }; } return session; } } }); ``` my login +page.server.ts (the form is passing an action="?/login" with use:enhance) ```ts import { fail, redirect, isRedirect } from '@sveltejs/kit'; import { z } from 'zod'; import { auth } from '$lib/server/auth'; import type { Actions } from './$types'; import { authClient } from '$lib/client/auth-client'; const LoginSchema = z.object({ email: z.email({ message: 'Please enter a valid email address.' }), password: z.string().min(1, 'Password is required.') }); export const actions: Actions = { email: async ({ request, fetch }) => { const formData = await request.formData(); const body = Object.fromEntries(formData); const result = LoginSchema.safeParse(body); if (!result.success) { return fail(400, { data: body, errors: result.error.flatten().fieldErrors }); } const { email, password } = result.data; try { const response = await fetch('/auth/sign-in/email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); if (response.ok) { throw redirect(303, '/dashboard'); } else { const errorData = await response.json().catch(() => ({})); console.error('Login failed:', errorData); if (errorData.message === 'User not verified') { throw redirect(303, `/auth/check-email?email=${email}&unverified=true`); } return fail(401, { data: body, errors: { _general: ['Invalid email or password. Please try again.'] } }); } } catch (err) { if (isRedirect(err)) { throw err; } console.error('Login Error:', err); return fail(500, { data: body, message: 'An internal server error occurred.' }); } }, google: async ({ request, url }) => { try { const response = await authClient.signIn.social({ provider: 'google', callbackURL: '/', errorCallbackURL: '/', newUserCallbackURL: '/' }); if (response.data && response.data.url) { throw redirect(303, response.data.url); } } catch (err) { if (isRedirect(err)) { throw err; } console.error('Google sign-in error:', err); return fail(500, { message: 'Could not sign in with Google.' }); } } }; ``` My authClient: ```ts import { env } from '$env/dynamic/private'; import { createAuthClient } from 'better-auth/svelte'; export const authClient = createAuthClient({ baseURL: env.BETTER_AUTH_URL, basePath: '/auth' }); ``` Authorized redirect on GCP are correctly configured under "/auth/callback/google" (since I am using basePath "auth") ### What version of Better Auth are you using? 1.3.28 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:34 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8103", "release": "24.6.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "8.00 GB", "freeMemory": "0.16 GB" }, "node": { "version": "v22.17.0", "env": "development" }, "packageManager": { "name": "npm", "version": "10.9.2" }, "frameworks": [ { "name": "svelte", "version": "^5.0.0" }, { "name": "@sveltejs/kit", "version": "^2.22.0" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "@neondatabase/serverless", "version": "^1.0.2" } ], "betterAuth": { "version": "1.3.28", "config": { "secret": "[REDACTED]", "baseURL": "http://localhost:5173", "basePath": "/auth", "database": { "_events": {}, "_eventsCount": 0, "options": { "connectionString": "[REDACTED]", "max": 20, "idleTimeoutMillis": 30000, "connectionTimeoutMillis": 10000, "min": 0, "maxUses": null, "allowExitOnIdle": false, "maxLifetimeSeconds": 0 }, "_clients": [], "_idle": [], "_expired": {}, "_pendingQueue": [], "ending": false, "ended": false }, "logger": { "level": "debug", "disabled": false }, "trustedOrigins": [ "http://localhost:5173", "https://sowarmly.com" ], "advanced": { "useSecureCookies": false, "cookiePrefix": "sowarmyly_auth_" }, "emailVerification": {}, "emailAndPassword": { "enabled": true, "requireEmailVerification": true }, "socialProviders": { "google": { "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "enabled": true } }, "session": { "cookieCache": { "enabled": true, "maxAge": 604800 } }, "callbacks": {} } } } ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context I am using Arc but I tried with Chrome and Safari
GiteaMirror added the svelte label 2026-03-13 09:32:06 -05:00
Author
Owner

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

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

This is a known issue with Better Auth 1.3.23+ and SvelteKit: the state_mismatch error almost always means the OAuth state cookie isn't being set or sent correctly during the Google sign-in flow. This is usually due to cookie settings or how the flow is initiated.

Here’s what you should check and update:

  1. Initiate social sign-in only from the client using authClient.signIn.social. Do not trigger it from a server action or API route—doing so will break the state/cookie logic and cause this error [source].

  2. Callback URLs:

    • In your Google Cloud Console, the OAuth redirect URI must be /auth/callback/google (matching your basePath).
    • In your signIn.social call, set callbackURL to a user-facing route (like / or /dashboard), not the callback endpoint itself. If you set it to /auth/callback/google, you’ll get a double callback and a state error [source].
  3. Cookie settings:

    • For cross-site OAuth flows, you must set the state cookie to SameSite=None and Secure=true so browsers send it on the callback. In your Better Auth config, add:
      advanced: {
        ...,
        cookies: {
          state: {
            attributes: {
              sameSite: "none",
              secure: true,
            }
          }
        }
      }
      
      This targets only the OAuth state cookie, keeping your session cookies at SameSite=Lax for CSRF protection [source].
  4. Trusted origins:

    • Make sure all your app URLs (local, preview, prod) are in trustedOrigins.
  5. If you use a custom basePath, ensure both your server and client configs match, and that the handler is mounted on the correct path [source].

If you still see the error after these changes, double-check that cookies are not being blocked by your browser or any extensions, and that your environment variables for baseURL and secret are set correctly.

Let me know if you need a code snippet or want to share your updated config for review!

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 (Oct 22, 2025): <!-- Greeting --> Hi @lorizz! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is a known issue with Better Auth 1.3.23+ and SvelteKit: the `state_mismatch` error almost always means the OAuth state cookie isn't being set or sent correctly during the Google sign-in flow. This is usually due to cookie settings or how the flow is initiated. Here’s what you should check and update: 1. **Initiate social sign-in only from the client** using `authClient.signIn.social`. Do not trigger it from a server action or API route—doing so will break the state/cookie logic and cause this error [[source]](https://github.com/better-auth/better-auth/issues/5055). 2. **Callback URLs:** - In your Google Cloud Console, the OAuth redirect URI must be `/auth/callback/google` (matching your `basePath`). - In your `signIn.social` call, set `callbackURL` to a user-facing route (like `/` or `/dashboard`), **not** the callback endpoint itself. If you set it to `/auth/callback/google`, you’ll get a double callback and a state error [[source]](https://github.com/better-auth/better-auth/issues/3488). 3. **Cookie settings:** - For cross-site OAuth flows, you must set the state cookie to `SameSite=None` and `Secure=true` so browsers send it on the callback. In your Better Auth config, add: ```ts advanced: { ..., cookies: { state: { attributes: { sameSite: "none", secure: true, } } } } ``` This targets only the OAuth state cookie, keeping your session cookies at `SameSite=Lax` for CSRF protection [[source]](https://github.com/better-auth/better-auth/issues/5243). 4. **Trusted origins:** - Make sure all your app URLs (local, preview, prod) are in `trustedOrigins`. 5. **If you use a custom `basePath`,** ensure both your server and client configs match, and that the handler is mounted on the correct path [[source]](https://github.com/better-auth/better-auth/issues/202). If you still see the error after these changes, double-check that cookies are not being blocked by your browser or any extensions, and that your environment variables for `baseURL` and `secret` are set correctly. Let me know if you need a code snippet or want to share your updated config for review! <!-- 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/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?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/5478)
Author
Owner

@lorizz commented on GitHub (Oct 22, 2025):

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

This is a known issue with Better Auth 1.3.23+ and SvelteKit: the state_mismatch error almost always means the OAuth state cookie isn't being set or sent correctly during the Google sign-in flow. This is usually due to cookie settings or how the flow is initiated.

Here’s what you should check and update:

  1. Initiate social sign-in only from the client using authClient.signIn.social. Do not trigger it from a server action or API route—doing so will break the state/cookie logic and cause this error [source].

  2. Callback URLs:

    • In your Google Cloud Console, the OAuth redirect URI must be /auth/callback/google (matching your basePath).
    • In your signIn.social call, set callbackURL to a user-facing route (like / or /dashboard), not the callback endpoint itself. If you set it to /auth/callback/google, you’ll get a double callback and a state error [source].
  3. Cookie settings:

    • For cross-site OAuth flows, you must set the state cookie to SameSite=None and Secure=true so browsers send it on the callback. In your Better Auth config, add:
      advanced: {
      ...,
      cookies: {
      state: {
      attributes: {
      sameSite: "none",
      secure: true,
      }
      }
      }
      }

      This targets only the OAuth state cookie, keeping your session cookies at SameSite=Lax for CSRF protection [source].

  4. Trusted origins:

    • Make sure all your app URLs (local, preview, prod) are in trustedOrigins.
  5. If you use a custom basePath, ensure both your server and client configs match, and that the handler is mounted on the correct path [source].

If you still see the error after these changes, double-check that cookies are not being blocked by your browser or any extensions, and that your environment variables for baseURL and secret are set correctly.

Let me know if you need a code snippet or want to share your updated config for review!

To reply, just mention @dosu.

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

Added the advanced config but it gives the same error:

Maybe there's something off in my hooks.server.ts?

// /hooks.server.ts
import { auth } from '$lib/server/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
import { redirect } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';
import type { Session as AppSession, User as AppUser } from '$lib/types';
import { env } from '$env/dynamic/private';
import { sql } from '$lib/server/db';
import Stripe from 'stripe';

let stripe: Stripe;
function getStripe() {
	if (!stripe && env.STRIPE_SECRET_KEY) {
		stripe = new Stripe(env.STRIPE_SECRET_KEY);
	}
	return stripe;
}

export const handle: Handle = async ({ event, resolve }) => {
	const sessionData = await auth.api.getSession({ headers: event.request.headers });

	if (sessionData?.user?.id) {
		try {
			const rows = await sql`
				SELECT id, name, email, role, credits, "stripeCustomerId", 
							"stripeSubscriptionId", "stripePlan" 
				FROM "user" 
				WHERE id = ${sessionData.user.id}
			`;
			const fullUser = rows[0] as AppUser | undefined;

			if (fullUser && fullUser.stripeSubscriptionId) {
				try {
					const subscription = await getStripe().subscriptions.retrieve(
						fullUser.stripeSubscriptionId
					);

					if (subscription.cancel_at) {
						fullUser.subscriptionStatus = 'canceled';
						const endDate = new Date(subscription.cancel_at * 1000);
						fullUser.subscriptionEndsAt = endDate.toLocaleDateString('it-IT', {
							day: '2-digit',
							month: '2-digit',
							year: 'numeric'
						});
					} else {
						fullUser.subscriptionStatus = 'active';
					}
				} catch (err) {
					console.error("Couldn't retrieve subscription status from Stripe:", err);
				}
			}

			if (fullUser) {
				event.locals.session = async (): Promise<AppSession | null> => ({
					...sessionData.session,
					user: fullUser
				});
			}
		} catch (err) {
			console.error('Error fetching user from DB:', err);
			event.locals.session = async () => null;
		}
	} else {
		event.locals.session = async () => null;
	}

	const url = event.url;
	const session = await event.locals.session();
	const isLoginOrRegisterPage = url.pathname === '/login' || url.pathname === '/register';

	if (session && isLoginOrRegisterPage) {
		throw redirect(302, '/dashboard');
	}

	const isProtectedRoute = url.pathname.startsWith('/dashboard');
	if (!session && isProtectedRoute) {
		throw redirect(302, '/login');
	}

	if (event.url.pathname.startsWith('/auth/')) {
		const customAuthPages = ['/auth/check-email', '/auth/verified'];
		if (customAuthPages.includes(event.url.pathname)) {
			return resolve(event);
		}

		return svelteKitHandler({ event, resolve, auth, building });
	}

	return resolve(event);
};

@dosu

@lorizz commented on GitHub (Oct 22, 2025): > Hi [@lorizz](https://github.com/lorizz)! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. > > This is a known issue with Better Auth 1.3.23+ and SvelteKit: the `state_mismatch` error almost always means the OAuth state cookie isn't being set or sent correctly during the Google sign-in flow. This is usually due to cookie settings or how the flow is initiated. > > Here’s what you should check and update: > > 1. **Initiate social sign-in only from the client** using `authClient.signIn.social`. Do not trigger it from a server action or API route—doing so will break the state/cookie logic and cause this error [[source]](https://github.com/better-auth/better-auth/issues/5055). > 2. **Callback URLs:** > > * In your Google Cloud Console, the OAuth redirect URI must be `/auth/callback/google` (matching your `basePath`). > * In your `signIn.social` call, set `callbackURL` to a user-facing route (like `/` or `/dashboard`), **not** the callback endpoint itself. If you set it to `/auth/callback/google`, you’ll get a double callback and a state error [[source]](https://github.com/better-auth/better-auth/issues/3488). > 3. **Cookie settings:** > > * For cross-site OAuth flows, you must set the state cookie to `SameSite=None` and `Secure=true` so browsers send it on the callback. In your Better Auth config, add: > advanced: { > ..., > cookies: { > state: { > attributes: { > sameSite: "none", > secure: true, > } > } > } > } > > > > > > > > > > This targets only the OAuth state cookie, keeping your session cookies at `SameSite=Lax` for CSRF protection [[source]](https://github.com/better-auth/better-auth/issues/5243). > 4. **Trusted origins:** > > * Make sure all your app URLs (local, preview, prod) are in `trustedOrigins`. > 5. **If you use a custom `basePath`,** ensure both your server and client configs match, and that the handler is mounted on the correct path [[source]](https://github.com/better-auth/better-auth/issues/202). > > If you still see the error after these changes, double-check that cookies are not being blocked by your browser or any extensions, and that your environment variables for `baseURL` and `secret` are set correctly. > > Let me know if you need a code snippet or want to share your updated config for review! > > _To reply, just mention [@dosu](https://go.dosu.dev/dosubot)._ > > How did I do? [Good](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/b50ac203-2553-461c-9f2b-f900f1b64520?feedback_type=other)  [![Chat with Dosu](https://camo.githubusercontent.com/d9ffebb979729c9ef0fc4e3090dbae5b17cabbfec8afe378ada6b81913d6ad8a/68747470733a2f2f646f73752e6465762f646f73752d636861742d62616467652e737667)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github) [![Join Discord](https://camo.githubusercontent.com/d562c2df5e4567ea41d37f1d91a4da0839d8341037045b103b2da268998de17f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a6f696e2d3538363546323f6c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d)](https://go.dosu.dev/discord-bot) [![Share on X](https://camo.githubusercontent.com/4649f705e2396d6f328dbc8e800a0509c9c17b3b20253b36706c393df124b93d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f582d73686172652d626c61636b)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5478) Added the advanced config but it gives the same error: Maybe there's something off in my hooks.server.ts? ```ts // /hooks.server.ts import { auth } from '$lib/server/auth'; import { svelteKitHandler } from 'better-auth/svelte-kit'; import { building } from '$app/environment'; import { redirect } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit'; import type { Session as AppSession, User as AppUser } from '$lib/types'; import { env } from '$env/dynamic/private'; import { sql } from '$lib/server/db'; import Stripe from 'stripe'; let stripe: Stripe; function getStripe() { if (!stripe && env.STRIPE_SECRET_KEY) { stripe = new Stripe(env.STRIPE_SECRET_KEY); } return stripe; } export const handle: Handle = async ({ event, resolve }) => { const sessionData = await auth.api.getSession({ headers: event.request.headers }); if (sessionData?.user?.id) { try { const rows = await sql` SELECT id, name, email, role, credits, "stripeCustomerId", "stripeSubscriptionId", "stripePlan" FROM "user" WHERE id = ${sessionData.user.id} `; const fullUser = rows[0] as AppUser | undefined; if (fullUser && fullUser.stripeSubscriptionId) { try { const subscription = await getStripe().subscriptions.retrieve( fullUser.stripeSubscriptionId ); if (subscription.cancel_at) { fullUser.subscriptionStatus = 'canceled'; const endDate = new Date(subscription.cancel_at * 1000); fullUser.subscriptionEndsAt = endDate.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' }); } else { fullUser.subscriptionStatus = 'active'; } } catch (err) { console.error("Couldn't retrieve subscription status from Stripe:", err); } } if (fullUser) { event.locals.session = async (): Promise<AppSession | null> => ({ ...sessionData.session, user: fullUser }); } } catch (err) { console.error('Error fetching user from DB:', err); event.locals.session = async () => null; } } else { event.locals.session = async () => null; } const url = event.url; const session = await event.locals.session(); const isLoginOrRegisterPage = url.pathname === '/login' || url.pathname === '/register'; if (session && isLoginOrRegisterPage) { throw redirect(302, '/dashboard'); } const isProtectedRoute = url.pathname.startsWith('/dashboard'); if (!session && isProtectedRoute) { throw redirect(302, '/login'); } if (event.url.pathname.startsWith('/auth/')) { const customAuthPages = ['/auth/check-email', '/auth/verified']; if (customAuthPages.includes(event.url.pathname)) { return resolve(event); } return svelteKitHandler({ event, resolve, auth, building }); } return resolve(event); }; ``` @dosu
Author
Owner

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

Your hooks.server.ts is likely causing the persistent state_mismatch error because it runs custom session logic (DB lookups, Stripe augmentation, redirects) before calling svelteKitHandler for /auth/* routes. For OAuth to work reliably in Better Auth 1.3.23+, svelteKitHandler must be the very first handler for all /auth/* requests—any logic before it can break cookie/state propagation and cause this error source.

To fix this, refactor your handle function so that for /auth/* routes, you immediately call svelteKitHandler({ event, resolve, auth, building }) and return its result, before any custom logic. Only run your session/user augmentation and redirects for non-auth routes, after authentication is handled.

Example pattern:

export const handle: Handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith('/auth/')) {
    // Always call svelteKitHandler first for auth routes
    return svelteKitHandler({ event, resolve, auth, building });
  }

  // Custom session logic for non-auth routes
  // ... your DB/user/Stripe logic here ...

  return resolve(event);
};

This ensures Better Auth can set and validate the OAuth state cookie correctly, preventing state_mismatch errors in Google sign-in flows 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 (Oct 22, 2025): <!-- Answer --> Your hooks.server.ts is likely causing the persistent `state_mismatch` error because it runs custom session logic (DB lookups, Stripe augmentation, redirects) before calling `svelteKitHandler` for `/auth/*` routes. For OAuth to work reliably in Better Auth 1.3.23+, `svelteKitHandler` must be the very first handler for all `/auth/*` requests—any logic before it can break cookie/state propagation and cause this error [source](https://github.com/better-auth/better-auth/blob/fa6ee9d68214c6a51e7253b53f957407b116b0c4/packages/better-auth/src/integrations/svelte-kit.ts). To fix this, refactor your handle function so that for `/auth/*` routes, you immediately call `svelteKitHandler({ event, resolve, auth, building })` and return its result, before any custom logic. Only run your session/user augmentation and redirects for non-auth routes, after authentication is handled. Example pattern: ```ts export const handle: Handle = async ({ event, resolve }) => { if (event.url.pathname.startsWith('/auth/')) { // Always call svelteKitHandler first for auth routes return svelteKitHandler({ event, resolve, auth, building }); } // Custom session logic for non-auth routes // ... your DB/user/Stripe logic here ... return resolve(event); }; ``` This ensures Better Auth can set and validate the OAuth state cookie correctly, preventing `state_mismatch` errors in Google sign-in flows [source](https://github.com/better-auth/better-auth/issues/5055). <!-- 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/81b2312a-af6e-4764-8d8e-c5d5e90489a6?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/81b2312a-af6e-4764-8d8e-c5d5e90489a6?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/81b2312a-af6e-4764-8d8e-c5d5e90489a6?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/81b2312a-af6e-4764-8d8e-c5d5e90489a6?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/81b2312a-af6e-4764-8d8e-c5d5e90489a6?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/81b2312a-af6e-4764-8d8e-c5d5e90489a6?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/81b2312a-af6e-4764-8d8e-c5d5e90489a6?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/5478)
Author
Owner

@lorizz commented on GitHub (Oct 22, 2025):

@dosu downgraded to 1.3.18, works perfectly, there's something broken in the latest build

@lorizz commented on GitHub (Oct 22, 2025): @dosu downgraded to 1.3.18, works perfectly, there's something broken in the latest build
Author
Owner

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

Your findings are spot on—this is a regression introduced after 1.3.18. Starting in 1.3.19 and especially 1.3.23+, Better Auth changed how OAuth state and cookies are handled, which broke some previously working SvelteKit and OAuth flows. The new versions require the OAuth state cookie to be set strictly on the client using authClient, and any deviation (like server-side initiation or custom middleware before the handler) can cause persistent state_mismatch errors. Multiple users have reported that downgrading to 1.3.18 or earlier resolves the issue, confirming a breaking change in later versions (details, more).

If you need to use a newer version, double-check that all social sign-in flows are initiated on the client, and that your SvelteKit middleware calls svelteKitHandler first for all /auth/* routes. There are also some temporary workarounds (like setting the state cookie to SameSite=None and Secure=true), but the underlying regression is in the new state/cookie logic (see this discussion). If you’re happy on 1.3.18, you’re not alone—many have stuck with it until a more robust fix lands.

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 (Oct 22, 2025): <!-- Answer --> Your findings are spot on—this is a regression introduced after 1.3.18. Starting in 1.3.19 and especially 1.3.23+, Better Auth changed how OAuth state and cookies are handled, which broke some previously working SvelteKit and OAuth flows. The new versions require the OAuth state cookie to be set strictly on the client using authClient, and any deviation (like server-side initiation or custom middleware before the handler) can cause persistent state_mismatch errors. Multiple users have reported that downgrading to 1.3.18 or earlier resolves the issue, confirming a breaking change in later versions ([details](https://github.com/better-auth/better-auth/issues/5055), [more](https://github.com/better-auth/better-auth/issues/4969)). If you need to use a newer version, double-check that all social sign-in flows are initiated on the client, and that your SvelteKit middleware calls svelteKitHandler first for all /auth/* routes. There are also some temporary workarounds (like setting the state cookie to SameSite=None and Secure=true), but the underlying regression is in the new state/cookie logic ([see this discussion](https://github.com/better-auth/better-auth/issues/4985)). If you’re happy on 1.3.18, you’re not alone—many have stuck with it until a more robust fix lands. <!-- 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/990050a0-c4fe-4e0a-8a03-d28708ec7183?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/990050a0-c4fe-4e0a-8a03-d28708ec7183?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/990050a0-c4fe-4e0a-8a03-d28708ec7183?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/990050a0-c4fe-4e0a-8a03-d28708ec7183?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/990050a0-c4fe-4e0a-8a03-d28708ec7183?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/990050a0-c4fe-4e0a-8a03-d28708ec7183?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/990050a0-c4fe-4e0a-8a03-d28708ec7183?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/5478)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2177