From fefbde28fe9052ea6138f954ca066198df50626b Mon Sep 17 00:00:00 2001 From: kimchi-developer Date: Sun, 1 Mar 2026 08:44:05 +0900 Subject: [PATCH] fix(expo): skip cookie/expo-origin headers for ID token requests (#7069) Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com> --- packages/expo/src/client.ts | 21 ++++++++++-- packages/expo/test/expo.test.ts | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/expo/src/client.ts b/packages/expo/src/client.ts index 5a8142a6d5..621e309302 100644 --- a/packages/expo/src/client.ts +++ b/packages/expo/src/client.ts @@ -415,9 +415,26 @@ export const expoClient = (opts: ExpoClientOptions) => { }; } options = options || {}; - const storedCookie = storage.getItem(cookieName); - const cookie = getCookie(storedCookie || "{}"); options.credentials = "omit"; + /** + * ID token flow (native sign-in) doesn't need cookie-based auth. + * The ID token itself is cryptographically signed by the provider + * and validated server-side, so no session cookies or origin + * validation is required. + * + * Sending cookie/expo-origin headers for ID token requests triggers + * unnecessary origin checks that fail for custom URL schemes. + */ + const isIdTokenRequest = options.body?.idToken !== undefined; + + if (isIdTokenRequest) { + options.headers = { + ...options.headers, + "x-skip-oauth-proxy": "true", + }; + } else { + const storedCookie = storage.getItem(cookieName); + const cookie = getCookie(storedCookie || "{}"); options.headers = { ...options.headers, ...(cookie ? { cookie } : {}), diff --git a/packages/expo/test/expo.test.ts b/packages/expo/test/expo.test.ts index b9f87fca8d..e28656a35b 100644 --- a/packages/expo/test/expo.test.ts +++ b/packages/expo/test/expo.test.ts @@ -417,6 +417,67 @@ describe("expo", async () => { expect(origin).toBe(null); }); + it("should not send cookie or expo-origin headers for ID token requests", async () => { + let cookieHeader: string | null | undefined = null; + let expoOriginHeader: string | null | undefined = null; + let originHeader: string | null | undefined = null; + const storage = new Map(); + + // Pre-populate storage with a cookie to verify it's NOT sent + storage.set( + "better-auth_cookie", + JSON.stringify({ + "better-auth.session_token": { + value: "existing-token", + expires: new Date(Date.now() + 1000 * 60 * 60).toISOString(), + }, + }), + ); + + const { client } = await getTestInstance( + { + hooks: { + before: createAuthMiddleware(async (ctx) => { + cookieHeader = ctx.request?.headers.get("cookie"); + expoOriginHeader = ctx.request?.headers.get("expo-origin"); + originHeader = ctx.request?.headers.get("origin"); + }), + }, + socialProviders: { + google: { + clientId: "test", + clientSecret: "test", + }, + }, + plugins: [expo()], + }, + { + clientOptions: { + plugins: [ + expoClient({ + storage: { + getItem: (key) => storage.get(key) || null, + setItem: async (key, value) => storage.set(key, value), + }, + }), + ], + }, + }, + ); + + // ID token request - should NOT have cookie or expo-origin headers + await client.signIn.social({ + provider: "google", + idToken: { + token: "fake-id-token", + }, + }); + + expect(cookieHeader).toBeNull(); + expect(expoOriginHeader).toBeNull(); + expect(originHeader).toBeNull(); + }); + it("should preserve existing cookies on link-social", async () => { await client.signIn.email({ email: testUser.email,