Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions packages/core/supabase-js/src/SupabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ export default class SupabaseClient<
accessToken: this._getAccessToken.bind(this),
...settings.realtime,
})
if (this.accessToken) {
setTimeout(() => {
this.accessToken?.()
?.then((token) => this.realtime.setAuth(token))
.catch((e) => console.warn('Failed to set initial Realtime auth token:', e))
}, 0)
}

this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
headers: this.headers,
schema: settings.db.schema,
Expand Down
35 changes: 32 additions & 3 deletions packages/core/supabase-js/test/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { assert } from 'console'
import { createClient, RealtimeChannel, SupabaseClient } from '../src/index'

import { sign } from 'jsonwebtoken'
// These tests assume that a local Supabase server is already running
// Start a local Supabase instance with 'supabase start' before running these tests
// Default local dev credentials from Supabase CLI
const SUPABASE_URL = 'http://127.0.0.1:54321'
const ANON_KEY =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0'

const JWT_SECRET = 'super-secret-jwt-token-with-at-least-32-characters-long'
// For Node.js < 22, we need to provide a WebSocket implementation
// Node.js 22+ has native WebSocket support
let wsTransport: any = undefined
Expand Down Expand Up @@ -292,7 +293,7 @@ describe('Supabase Integration Tests', () => {

channel
.on('broadcast', { event: '*' }, (payload) => (receivedMessage = payload))
.subscribe((status) => {
.subscribe((status, err) => {
if (status == 'SUBSCRIBED') subscribed = true
})

Expand Down Expand Up @@ -358,3 +359,31 @@ describe('Storage API', () => {
expect(deleteError).toBeNull()
})
})

describe('Custom JWT', () => {
describe('Realtime', () => {
test('will connect with a properly signed jwt token', async () => {
const jwtToken = sign({ sub: '1234567890' }, JWT_SECRET, { expiresIn: '1h' })
const supabaseWithCustomJwt = createClient(SUPABASE_URL, ANON_KEY, {
accessToken: () => Promise.resolve(jwtToken),
})
await new Promise((resolve) => setTimeout(resolve, 100))
expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken)
let subscribed = false
let attempts = 0
supabaseWithCustomJwt.channel('test-channel').subscribe((status) => {
if (status == 'SUBSCRIBED') subscribed = true
})

// Wait for subscription
while (!subscribed) {
if (attempts > 50) throw new Error('Timeout waiting for subscription')
await new Promise((resolve) => setTimeout(resolve, 100))
attempts++
}

expect(subscribed).toBe(true)
//
}, 10000)
})
})
55 changes: 55 additions & 0 deletions packages/core/supabase-js/test/unit/SupabaseClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,61 @@ describe('SupabaseClient', () => {
})

describe('Realtime Authentication', () => {
test('should automatically call setAuth() when accessToken option is provided', async () => {
const customToken = 'custom-jwt-token'
const customAccessTokenFn = jest.fn().mockResolvedValue(customToken)
const client = createClient(URL, KEY, { accessToken: customAccessTokenFn })

await new Promise((resolve) => setTimeout(resolve, 0))

expect((client.realtime as any).accessTokenValue).toBe(customToken)
expect(customAccessTokenFn).toHaveBeenCalled()
})

test('should automatically populate token in channels when using custom JWT', async () => {
const customToken = 'custom-channel-token'
const customAccessTokenFn = jest.fn().mockResolvedValue(customToken)
const client = createClient(URL, KEY, { accessToken: customAccessTokenFn })

await new Promise((resolve) => setTimeout(resolve, 0))

const channel = client.channel('test-channel')
channel.subscribe()

expect((channel as any).joinPush.payload.access_token).toBe(customToken)
expect((client.realtime as any).accessTokenValue).toBe(customToken)
})

test('should handle errors gracefully when accessToken callback fails', async () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
const error = new Error('Token fetch failed')
const failingAccessTokenFn = jest.fn().mockRejectedValue(error)

const client = createClient(URL, KEY, { accessToken: failingAccessTokenFn })

await new Promise((resolve) => setTimeout(resolve, 0))

expect(consoleWarnSpy).toHaveBeenCalledWith(
'Failed to set initial Realtime auth token:',
error
)
expect(client).toBeDefined()
expect(client.realtime).toBeDefined()

consoleWarnSpy.mockRestore()
})

test('should not call setAuth() automatically in normal mode', async () => {
const client = createClient(URL, KEY)
const setAuthSpy = jest.spyOn(client.realtime, 'setAuth')

await new Promise((resolve) => setTimeout(resolve, 10))

expect(setAuthSpy).not.toHaveBeenCalled()

setAuthSpy.mockRestore()
})

test('should provide access token to realtime client', async () => {
const expectedToken = 'test-jwt-token'
const client = createClient(URL, KEY)
Expand Down
1 change: 1 addition & 0 deletions supabase/.branches/_current_branch
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
main
1 change: 1 addition & 0 deletions supabase/.temp/cli-latest
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v2.54.11
Loading