diff --git a/packages/core/auth-js/src/GoTrueClient.ts b/packages/core/auth-js/src/GoTrueClient.ts index c61e3483a..2045b7dd8 100644 --- a/packages/core/auth-js/src/GoTrueClient.ts +++ b/packages/core/auth-js/src/GoTrueClient.ts @@ -2111,6 +2111,14 @@ export default class GoTrueClient { if (typeof this.detectSessionInUrl === 'function') { return this.detectSessionInUrl(new URL(window.location.href), params) } + // Check for Supabase Auth identifier + if ('sb' in params) { + // sb is just an identifier + // Still require OAuth params to prevent forced logout via crafted URLs with only 'sb' + return Boolean(params.access_token || params.error || params.error_description) + } + // TODO @mandarini: Remove this legacy fallback in next major version and return false instead + // Legacy detection for backwards compatibility with older Auth servers that don't include 'sb' return Boolean(params.access_token || params.error_description) } diff --git a/packages/core/auth-js/src/lib/types.ts b/packages/core/auth-js/src/lib/types.ts index 86a7dfe74..e13479192 100644 --- a/packages/core/auth-js/src/lib/types.ts +++ b/packages/core/auth-js/src/lib/types.ts @@ -84,15 +84,23 @@ export type GoTrueClientOptions = { * The function receives the current URL and parsed parameters, and should return true if the URL * should be processed as a Supabase auth callback, or false to ignore it. * + * By default, the client checks for the `sb` parameter (added by Supabase Auth server) to identify + * Supabase callbacks, with a fallback to legacy detection for older Auth server versions. + * * This is useful when your app uses other OAuth providers (e.g., Facebook Login) that also return * access_token in the URL fragment, which would otherwise be incorrectly intercepted by Supabase Auth. * * @example * ```ts * detectSessionInUrl: (url, params) => { - * // Ignore Facebook OAuth redirects + * // Ignore known third-party OAuth paths * if (url.pathname === '/facebook/redirect') return false - * // Use default detection for other URLs + * // Check for sb identifier (available on newer Auth servers) + * // Still require OAuth params to prevent issues with crafted URLs + * if ('sb' in params) { + * return Boolean(params.access_token || params.error || params.error_description) + * } + * // Fall back to legacy detection for older Auth servers * return Boolean(params.access_token || params.error_description) * } * ``` diff --git a/packages/core/auth-js/test/GoTrueClient.browser.test.ts b/packages/core/auth-js/test/GoTrueClient.browser.test.ts index 4ebe0dcb1..b5181fb9b 100644 --- a/packages/core/auth-js/test/GoTrueClient.browser.test.ts +++ b/packages/core/auth-js/test/GoTrueClient.browser.test.ts @@ -580,6 +580,97 @@ describe('Callback URL handling', () => { expect(data.session).toBeDefined() expect(data.session?.access_token).toBe('test-token') }) + + it('should detect Supabase callback with sb parameter', async () => { + // Simulate Supabase OAuth redirect with sb identifier + window.location.href = + 'http://localhost:9999/callback#access_token=test-token&refresh_token=test-refresh&expires_in=3600&token_type=bearer&sb' + + // Mock fetch for user info + mockFetch.mockImplementation((url: string) => { + if (url.includes('/user')) { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + id: 'test-user', + email: 'test@example.com', + created_at: new Date().toISOString(), + }), + }) + } + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) + }) + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + detectSessionInUrl: true, + autoRefreshToken: false, + storage: mockStorage, + }) + + await client.initialize() + + // Should process the callback because sb parameter is present + const { data } = await client.getSession() + expect(data.session).toBeDefined() + expect(data.session?.access_token).toBe('test-token') + }) + + it('should detect legacy callbacks without sb parameter for backwards compatibility', async () => { + // Simulate legacy Supabase OAuth redirect without sb identifier + window.location.href = + 'http://localhost:9999/callback#access_token=test-token&refresh_token=test-refresh&expires_in=3600&token_type=bearer' + + // Mock fetch for user info + mockFetch.mockImplementation((url: string) => { + if (url.includes('/user')) { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + id: 'test-user', + email: 'test@example.com', + created_at: new Date().toISOString(), + }), + }) + } + return Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) + }) + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + detectSessionInUrl: true, + autoRefreshToken: false, + storage: mockStorage, + }) + + await client.initialize() + + // Should still process the callback for backwards compatibility + const { data } = await client.getSession() + expect(data.session).toBeDefined() + expect(data.session?.access_token).toBe('test-token') + }) + + it('should detect error callbacks with sb parameter', async () => { + // Simulate Supabase OAuth error redirect with sb identifier + window.location.href = + 'http://localhost:9999/callback#error=access_denied&error_description=User%20denied%20access&sb' + + const client = new (require('../src/GoTrueClient').default)({ + url: 'http://localhost:9999', + detectSessionInUrl: true, + autoRefreshToken: false, + storage: mockStorage, + }) + + const { error } = await client.initialize() + + // Should detect and process the error callback + expect(error).toBeDefined() + expect(error?.message).toContain('User denied access') + }) }) describe('GoTrueClient BroadcastChannel', () => { diff --git a/packages/core/supabase-js/src/lib/types.ts b/packages/core/supabase-js/src/lib/types.ts index 55cea95cd..d1a3c191b 100644 --- a/packages/core/supabase-js/src/lib/types.ts +++ b/packages/core/supabase-js/src/lib/types.ts @@ -53,6 +53,9 @@ export type SupabaseClientOptions = { * a Supabase auth callback. The function receives the current URL and parsed parameters, * and should return true if the URL should be processed as a Supabase auth callback. * + * By default, the client checks for the `sb` parameter (added by Supabase Auth server) to identify + * Supabase callbacks, with a fallback to legacy detection for older Auth server versions. + * * This is useful when your app uses other OAuth providers (e.g., Facebook Login) that * also return access_token in the URL fragment, which would otherwise be incorrectly * intercepted by Supabase Auth. @@ -60,9 +63,14 @@ export type SupabaseClientOptions = { * @example * ```ts * detectSessionInUrl: (url, params) => { - * // Ignore Facebook OAuth redirects + * // Ignore known third-party OAuth paths * if (url.pathname === '/facebook/redirect') return false - * // Use default detection for other URLs + * // Check for sb identifier (available on newer Auth servers) + * // Still require OAuth params to prevent issues with crafted URLs + * if ('sb' in params) { + * return Boolean(params.access_token || params.error || params.error_description) + * } + * // Fall back to legacy detection for older Auth servers * return Boolean(params.access_token || params.error_description) * } * ```