Skip to content

Commit 52f0184

Browse files
committed
fix(realtime): setAuth not required on custom jwt token
When a custom jwt token is provided we no longer require setAuth to be called with the custom jwt
1 parent 7511686 commit 52f0184

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

packages/core/supabase-js/src/SupabaseClient.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ export default class SupabaseClient<
156156
accessToken: this._getAccessToken.bind(this),
157157
...settings.realtime,
158158
})
159+
if (this.accessToken) {
160+
this.realtime.setAuth().catch((e) => {
161+
console.warn('Failed to set initial Realtime auth token:', e)
162+
})
163+
}
159164
this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
160165
headers: this.headers,
161166
schema: settings.db.schema,

packages/core/supabase-js/test/integration.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,52 @@ describe('Supabase Integration Tests', () => {
316316
expect(receivedMessage).toBeDefined()
317317
expect(supabase.realtime.getChannels().length).toBe(1)
318318
}, 10000)
319+
320+
test('should automatically set auth token when using custom JWT without manual setAuth()', async () => {
321+
// Sign up a user with the normal client to get a real JWT token
322+
await supabase.auth.signOut()
323+
const email = `custom-jwt-${Date.now()}@example.com`
324+
const password = 'password123'
325+
const { data: signUpData } = await supabase.auth.signUp({ email, password })
326+
expect(signUpData.session).toBeDefined()
327+
const realJwtToken = signUpData.session!.access_token
328+
329+
const customJwtClient = createClient(SUPABASE_URL, ANON_KEY, {
330+
accessToken: async () => realJwtToken,
331+
realtime: {
332+
heartbeatIntervalMs: 500,
333+
...(wsTransport && { transport: wsTransport }),
334+
},
335+
})
336+
337+
await new Promise((resolve) => setTimeout(resolve, 100))
338+
339+
expect((customJwtClient.realtime as any).accessTokenValue).toBe(realJwtToken)
340+
341+
const customChannelName = `custom-jwt-channel-${crypto.randomUUID()}`
342+
const config = { broadcast: { self: true }, private: true }
343+
const customChannel = customJwtClient.channel(customChannelName, { config })
344+
345+
expect((customChannel as any).joinPush.payload.access_token).toBe(realJwtToken)
346+
347+
let subscribed = false
348+
let attempts = 0
349+
350+
customChannel.subscribe((status) => {
351+
if (status == 'SUBSCRIBED') subscribed = true
352+
})
353+
354+
while (!subscribed) {
355+
if (attempts > 50) throw new Error('Timeout waiting for subscription')
356+
await new Promise((resolve) => setTimeout(resolve, 100))
357+
attempts++
358+
}
359+
360+
expect(subscribed).toBe(true)
361+
expect(customJwtClient.realtime.getChannels().length).toBe(1)
362+
363+
await customJwtClient.removeAllChannels()
364+
}, 10000)
319365
})
320366
})
321367

packages/core/supabase-js/test/unit/SupabaseClient.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,61 @@ describe('SupabaseClient', () => {
259259
})
260260

261261
describe('Realtime Authentication', () => {
262+
test('should automatically call setAuth() when accessToken option is provided', async () => {
263+
const customToken = 'custom-jwt-token'
264+
const customAccessTokenFn = jest.fn().mockResolvedValue(customToken)
265+
const client = createClient(URL, KEY, { accessToken: customAccessTokenFn })
266+
267+
await new Promise((resolve) => setTimeout(resolve, 0))
268+
269+
expect((client.realtime as any).accessTokenValue).toBe(customToken)
270+
expect(customAccessTokenFn).toHaveBeenCalled()
271+
})
272+
273+
test('should automatically populate token in channels when using custom JWT', async () => {
274+
const customToken = 'custom-channel-token'
275+
const customAccessTokenFn = jest.fn().mockResolvedValue(customToken)
276+
const client = createClient(URL, KEY, { accessToken: customAccessTokenFn })
277+
278+
await new Promise((resolve) => setTimeout(resolve, 0))
279+
280+
const channel = client.channel('test-channel')
281+
channel.subscribe()
282+
283+
expect((channel as any).joinPush.payload.access_token).toBe(customToken)
284+
expect((client.realtime as any).accessTokenValue).toBe(customToken)
285+
})
286+
287+
test('should handle errors gracefully when accessToken callback fails', async () => {
288+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
289+
const error = new Error('Token fetch failed')
290+
const failingAccessTokenFn = jest.fn().mockRejectedValue(error)
291+
292+
const client = createClient(URL, KEY, { accessToken: failingAccessTokenFn })
293+
294+
await new Promise((resolve) => setTimeout(resolve, 0))
295+
296+
expect(consoleWarnSpy).toHaveBeenCalledWith(
297+
'Failed to set initial Realtime auth token:',
298+
error
299+
)
300+
expect(client).toBeDefined()
301+
expect(client.realtime).toBeDefined()
302+
303+
consoleWarnSpy.mockRestore()
304+
})
305+
306+
test('should not call setAuth() automatically in normal mode', async () => {
307+
const client = createClient(URL, KEY)
308+
const setAuthSpy = jest.spyOn(client.realtime, 'setAuth')
309+
310+
await new Promise((resolve) => setTimeout(resolve, 10))
311+
312+
expect(setAuthSpy).not.toHaveBeenCalled()
313+
314+
setAuthSpy.mockRestore()
315+
})
316+
262317
test('should provide access token to realtime client', async () => {
263318
const expectedToken = 'test-jwt-token'
264319
const client = createClient(URL, KEY)

0 commit comments

Comments
 (0)