Skip to content

Commit f346169

Browse files
authored
feat(auth): allow custom predicate for detectSessionInUrl option (#1958)
1 parent 02c3224 commit f346169

File tree

4 files changed

+202
-4
lines changed

4 files changed

+202
-4
lines changed

packages/core/auth-js/src/GoTrueClient.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ export default class GoTrueClient {
255255
* Keep extra care to never reject or throw uncaught errors
256256
*/
257257
protected initializePromise: Promise<InitializeResult> | null = null
258-
protected detectSessionInUrl = true
258+
protected detectSessionInUrl:
259+
| boolean
260+
| ((url: URL, params: { [parameter: string]: string }) => boolean) = true
259261
protected url: string
260262
protected headers: {
261263
[key: string]: string
@@ -2100,8 +2102,15 @@ export default class GoTrueClient {
21002102

21012103
/**
21022104
* Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2)
2105+
*
2106+
* If `detectSessionInUrl` is a function, it will be called with the URL and params to determine
2107+
* if the URL should be processed as a Supabase auth callback. This allows users to exclude
2108+
* URLs from other OAuth providers (e.g., Facebook Login) that also return access_token in the fragment.
21032109
*/
21042110
private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
2111+
if (typeof this.detectSessionInUrl === 'function') {
2112+
return this.detectSessionInUrl(new URL(window.location.href), params)
2113+
}
21052114
return Boolean(params.access_token || params.error_description)
21062115
}
21072116

packages/core/auth-js/src/lib/types.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,27 @@ export type GoTrueClientOptions = {
7474
headers?: { [key: string]: string }
7575
/* Optional key name used for storing tokens in local storage. */
7676
storageKey?: string
77-
/* Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user. */
78-
detectSessionInUrl?: boolean
77+
/**
78+
* Set to "true" if you want to automatically detect OAuth grants in the URL and sign in the user.
79+
* Set to "false" to disable automatic detection.
80+
* Set to a function to provide custom logic for determining if a URL contains a Supabase auth callback.
81+
* The function receives the current URL and parsed parameters, and should return true if the URL
82+
* should be processed as a Supabase auth callback, or false to ignore it.
83+
*
84+
* This is useful when your app uses other OAuth providers (e.g., Facebook Login) that also return
85+
* access_token in the URL fragment, which would otherwise be incorrectly intercepted by Supabase Auth.
86+
*
87+
* @example
88+
* ```ts
89+
* detectSessionInUrl: (url, params) => {
90+
* // Ignore Facebook OAuth redirects
91+
* if (url.pathname === '/facebook/redirect') return false
92+
* // Use default detection for other URLs
93+
* return Boolean(params.access_token || params.error_description)
94+
* }
95+
* ```
96+
*/
97+
detectSessionInUrl?: boolean | ((url: URL, params: { [parameter: string]: string }) => boolean)
7998
/* Set to "true" if you want to automatically refresh the token before expiring. */
8099
autoRefreshToken?: boolean
81100
/* Set to "true" if you want to automatically save the user session into local storage. If set to false, session will just be saved in memory. */

packages/core/auth-js/test/GoTrueClient.browser.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,158 @@ describe('Callback URL handling', () => {
428428

429429
expect(client).toBeDefined()
430430
})
431+
432+
it('should use custom detectSessionInUrl function to filter out non-Supabase OAuth callbacks', async () => {
433+
// Simulate Facebook OAuth redirect with access_token in fragment
434+
window.location.href =
435+
'http://localhost:9999/facebook/redirect#access_token=facebook-token&data_access_expiration_time=1658889585'
436+
437+
// Custom predicate to ignore Facebook OAuth redirects
438+
const detectSessionInUrlFn = jest.fn((url: URL, params: { [key: string]: string }) => {
439+
// Ignore Facebook OAuth redirects
440+
if (url.pathname === '/facebook/redirect') return false
441+
// Default behavior for other URLs
442+
return Boolean(params.access_token || params.error_description)
443+
})
444+
445+
const client = new (require('../src/GoTrueClient').default)({
446+
url: 'http://localhost:9999',
447+
detectSessionInUrl: detectSessionInUrlFn,
448+
autoRefreshToken: false,
449+
storage: mockStorage,
450+
})
451+
452+
await client.initialize()
453+
454+
// The custom function should have been called
455+
expect(detectSessionInUrlFn).toHaveBeenCalled()
456+
expect(detectSessionInUrlFn).toHaveBeenCalledWith(
457+
expect.any(URL),
458+
expect.objectContaining({ access_token: 'facebook-token' })
459+
)
460+
461+
// Session should be null because we filtered out the Facebook callback
462+
const { data } = await client.getSession()
463+
expect(data.session).toBeNull()
464+
})
465+
466+
it('should process Supabase callbacks when custom detectSessionInUrl returns true', async () => {
467+
// Simulate Supabase OAuth redirect
468+
window.location.href =
469+
'http://localhost:9999/auth/callback#access_token=supabase-token&refresh_token=test-refresh&expires_in=3600&token_type=bearer&type=implicit'
470+
471+
// Mock fetch for user info
472+
mockFetch.mockImplementation((url: string) => {
473+
if (url.includes('/user')) {
474+
return Promise.resolve({
475+
ok: true,
476+
json: () =>
477+
Promise.resolve({
478+
id: 'test-user',
479+
480+
created_at: new Date().toISOString(),
481+
}),
482+
})
483+
}
484+
return Promise.resolve({
485+
ok: true,
486+
json: () =>
487+
Promise.resolve({
488+
access_token: 'supabase-token',
489+
refresh_token: 'test-refresh',
490+
expires_in: 3600,
491+
token_type: 'bearer',
492+
user: { id: 'test-user' },
493+
}),
494+
})
495+
})
496+
497+
// Custom predicate that allows Supabase callbacks but not Facebook
498+
const detectSessionInUrlFn = jest.fn((url: URL, params: { [key: string]: string }) => {
499+
if (url.pathname === '/facebook/redirect') return false
500+
return Boolean(params.access_token || params.error_description)
501+
})
502+
503+
const client = new (require('../src/GoTrueClient').default)({
504+
url: 'http://localhost:9999',
505+
detectSessionInUrl: detectSessionInUrlFn,
506+
autoRefreshToken: false,
507+
storage: mockStorage,
508+
})
509+
510+
await client.initialize()
511+
512+
// The custom function should have been called and returned true
513+
expect(detectSessionInUrlFn).toHaveBeenCalled()
514+
expect(detectSessionInUrlFn.mock.results[0].value).toBe(true)
515+
516+
// Session should be set because we allowed this callback
517+
const { data } = await client.getSession()
518+
expect(data.session).toBeDefined()
519+
expect(data.session?.access_token).toBe('supabase-token')
520+
})
521+
522+
it('should return error when custom detectSessionInUrl function throws', async () => {
523+
window.location.href = 'http://localhost:9999/callback#access_token=test-token'
524+
525+
// Reset storage state from previous tests
526+
storedSession = null
527+
528+
// Custom predicate that throws an error
529+
const detectSessionInUrlFn = jest.fn(() => {
530+
throw new Error('Custom predicate error')
531+
})
532+
533+
const client = new (require('../src/GoTrueClient').default)({
534+
url: 'http://localhost:9999',
535+
detectSessionInUrl: detectSessionInUrlFn,
536+
autoRefreshToken: false,
537+
storage: mockStorage,
538+
})
539+
540+
// initialize() catches errors and returns them wrapped in AuthUnknownError
541+
const { error } = await client.initialize()
542+
543+
expect(detectSessionInUrlFn).toHaveBeenCalled()
544+
expect(error).toBeDefined()
545+
expect(error?.message).toBe('Unexpected error during initialization')
546+
expect(error?.originalError?.message).toBe('Custom predicate error')
547+
})
548+
549+
it('should use default behavior when detectSessionInUrl is true (boolean)', async () => {
550+
window.location.href =
551+
'http://localhost:9999/callback#access_token=test-token&refresh_token=test-refresh&expires_in=3600&token_type=bearer&type=implicit'
552+
553+
// Mock fetch for user info
554+
mockFetch.mockImplementation((url: string) => {
555+
if (url.includes('/user')) {
556+
return Promise.resolve({
557+
ok: true,
558+
json: () =>
559+
Promise.resolve({
560+
id: 'test-user',
561+
562+
created_at: new Date().toISOString(),
563+
}),
564+
})
565+
}
566+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
567+
})
568+
569+
const client = new (require('../src/GoTrueClient').default)({
570+
url: 'http://localhost:9999',
571+
detectSessionInUrl: true, // Boolean true - use default behavior
572+
autoRefreshToken: false,
573+
storage: mockStorage,
574+
})
575+
576+
await client.initialize()
577+
578+
// Should process the callback with default behavior
579+
const { data } = await client.getSession()
580+
expect(data.session).toBeDefined()
581+
expect(data.session?.access_token).toBe('test-token')
582+
})
431583
})
432584

433585
describe('GoTrueClient BroadcastChannel', () => {

packages/core/supabase-js/src/lib/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,26 @@ export type SupabaseClientOptions<SchemaName> = {
4848
persistSession?: boolean
4949
/**
5050
* Detect a session from the URL. Used for OAuth login callbacks. Defaults to true.
51+
*
52+
* Can be set to a function to provide custom logic for determining if a URL contains
53+
* a Supabase auth callback. The function receives the current URL and parsed parameters,
54+
* and should return true if the URL should be processed as a Supabase auth callback.
55+
*
56+
* This is useful when your app uses other OAuth providers (e.g., Facebook Login) that
57+
* also return access_token in the URL fragment, which would otherwise be incorrectly
58+
* intercepted by Supabase Auth.
59+
*
60+
* @example
61+
* ```ts
62+
* detectSessionInUrl: (url, params) => {
63+
* // Ignore Facebook OAuth redirects
64+
* if (url.pathname === '/facebook/redirect') return false
65+
* // Use default detection for other URLs
66+
* return Boolean(params.access_token || params.error_description)
67+
* }
68+
* ```
5169
*/
52-
detectSessionInUrl?: boolean
70+
detectSessionInUrl?: boolean | ((url: URL, params: { [parameter: string]: string }) => boolean)
5371
/**
5472
* A storage provider. Used to store the logged-in session.
5573
*/

0 commit comments

Comments
 (0)