Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/core/supabase-js/src/SupabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class SupabaseClient<
protected accessToken?: () => Promise<string | null>

protected headers: Record<string, string>
protected bypassAuthSession: boolean

/**
* Create a new client for use in the browser.
Expand Down Expand Up @@ -136,6 +137,7 @@ export default class SupabaseClient<

this.storageKey = settings.auth.storageKey ?? ''
this.headers = settings.global.headers ?? {}
this.bypassAuthSession = settings.global.bypassAuthSession ?? false

if (!settings.accessToken) {
this.auth = this._initSupabaseAuthClient(
Expand Down Expand Up @@ -341,6 +343,14 @@ export default class SupabaseClient<
return await this.accessToken()
}

// When bypassAuthSession is true, always use the API key (e.g., service_role key)
// instead of any active user session token. This ensures requests run with
// the permissions of the API key, which is essential for service-role clients
// accessing security_invoker views or performing admin operations.
if (this.bypassAuthSession) {
return this.supabaseKey
}

const { data } = await this.auth.getSession()

return data.session?.access_token ?? this.supabaseKey
Expand Down
21 changes: 21 additions & 0 deletions packages/core/supabase-js/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ export type SupabaseClientOptions<SchemaName> = {
* Optional headers for initializing the client.
*/
headers?: Record<string, string>
/**
* When set to `true`, the client will always use the API key for the
* `Authorization` header, ignoring any active user session. This is
* useful for service-role clients (admin clients) that should operate
* with elevated privileges regardless of user session state.
*
* When using a service-role key with `security_invoker` views or
* functions that require service-role permissions, set this to `true`
* to ensure requests always run with service-role privileges.
*
* @default false
*
* @example
* ```ts
* // Create an admin client that always uses the service-role key
* const adminClient = createClient(url, serviceRoleKey, {
* global: { bypassAuthSession: true }
* })
* ```
*/
bypassAuthSession?: boolean
}
/**
* Optional function for using a third-party authentication system with
Expand Down
72 changes: 72 additions & 0 deletions packages/core/supabase-js/test/unit/SupabaseClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,78 @@ describe('SupabaseClient', () => {
})
})

describe('Bypass Auth Session', () => {
test('should use supabaseKey when bypassAuthSession is true, even with active session', async () => {
const sessionToken = 'user-session-token'
const client = createClient(URL, KEY, {
global: { bypassAuthSession: true },
})

// Mock an active session
client.auth.getSession = jest.fn().mockResolvedValue({
data: { session: { access_token: sessionToken } },
})

// @ts-ignore - accessing private method
const token = await client._getAccessToken()
// Should use the API key, not the session token
expect(token).toBe(KEY)
// getSession should not be called when bypassAuthSession is true
expect(client.auth.getSession).not.toHaveBeenCalled()
})

test('should use session token when bypassAuthSession is false (default)', async () => {
const sessionToken = 'user-session-token'
const client = createClient(URL, KEY)

client.auth.getSession = jest.fn().mockResolvedValue({
data: { session: { access_token: sessionToken } },
})

// @ts-ignore - accessing private method
const token = await client._getAccessToken()
expect(token).toBe(sessionToken)
expect(client.auth.getSession).toHaveBeenCalled()
})

test('should pass correct token with bypassAuthSession in fetchWithAuth', async () => {
const sessionToken = 'user-session-token'
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({}),
})

const client = createClient(URL, KEY, {
global: { fetch: mockFetch, bypassAuthSession: true },
})

// Mock an active session that would normally override the key
client.auth.getSession = jest.fn().mockResolvedValue({
data: { session: { access_token: sessionToken } },
})

await client.from('test').select('*')

expect(mockFetch).toHaveBeenCalled()
const [, options] = mockFetch.mock.calls[0]
// Authorization should use the API key, not the session token
expect(options.headers.get('Authorization')).toBe(`Bearer ${KEY}`)
expect(options.headers.get('apikey')).toBe(KEY)
})

test('should store bypassAuthSession value correctly', () => {
const clientWithBypass = createClient(URL, KEY, {
global: { bypassAuthSession: true },
})
// @ts-ignore - accessing protected property
expect(clientWithBypass.bypassAuthSession).toBe(true)

const clientWithoutBypass = createClient(URL, KEY)
// @ts-ignore - accessing protected property
expect(clientWithoutBypass.bypassAuthSession).toBe(false)
})
})

describe('Realtime Authentication', () => {
afterEach(() => {
jest.clearAllMocks()
Expand Down