From a311ae983e08bb98d9344e7bff30c264cf9f875c Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sun, 12 Jul 2026 17:46:41 -0600 Subject: [PATCH] Normalize websocket bearer tokens --- src/lib/websocket/middleware/auth.js | 16 +++++- src/lib/websocket/middleware/auth.test.js | 68 +++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/lib/websocket/middleware/auth.test.js diff --git a/src/lib/websocket/middleware/auth.js b/src/lib/websocket/middleware/auth.js index 9d677452..f672791d 100644 --- a/src/lib/websocket/middleware/auth.js +++ b/src/lib/websocket/middleware/auth.js @@ -5,6 +5,15 @@ import { createClient } from '@supabase/supabase-js'; +function getBearerToken(authHeader) { + if (typeof authHeader !== 'string') return null; + + const match = authHeader.match(/^Bearer\s+(.+)$/i); + const token = match?.[1]?.trim(); + + return token || null; +} + /** * Extract JWT token from WebSocket request * @param {Object} request - WebSocket upgrade request @@ -13,8 +22,9 @@ import { createClient } from '@supabase/supabase-js'; function extractToken(request) { // Try to get token from Authorization header const authHeader = request.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - return authHeader.substring(7); + const bearerToken = getBearerToken(authHeader); + if (bearerToken) { + return bearerToken; } // Try to get token from query parameters @@ -172,4 +182,4 @@ export async function canAccessGroup(userId, groupId, supabase) { console.error('Error checking group access:', error); return false; } -} \ No newline at end of file +} diff --git a/src/lib/websocket/middleware/auth.test.js b/src/lib/websocket/middleware/auth.test.js new file mode 100644 index 00000000..f096a62f --- /dev/null +++ b/src/lib/websocket/middleware/auth.test.js @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn() +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(() => ({ + auth: { + getUser: mocks.authGetUser + } + })) +})); + +function websocketRequest({ authorization, url = '/socket' }) { + return { + url, + headers: { + host: 'qrypt.chat', + ...(authorization ? { authorization } : {}) + } + }; +} + +describe('authenticateWebSocket bearer token parsing', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + process.env.PUBLIC_SUPABASE_URL = 'https://example.supabase.co'; + process.env.PUBLIC_SUPABASE_ANON_KEY = 'anon-key'; + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + }); + + it('normalizes bearer scheme casing and extra spaces', async () => { + const { authenticateWebSocket } = await import('./auth.js'); + const result = await authenticateWebSocket( + websocketRequest({ authorization: 'bearer access-token ' }) + ); + + expect(result.success).toBe(true); + expect(result.user).toMatchObject({ + id: 'auth-user-id', + access_token: 'access-token' + }); + expect(mocks.authGetUser).toHaveBeenCalledWith('access-token'); + }); + + it('ignores an empty bearer header and falls back to query tokens', async () => { + const { authenticateWebSocket } = await import('./auth.js'); + const result = await authenticateWebSocket( + websocketRequest({ + authorization: 'Bearer ', + url: '/socket?token=query-token' + }) + ); + + expect(result.success).toBe(true); + expect(result.user).toMatchObject({ + id: 'auth-user-id', + access_token: 'query-token' + }); + expect(mocks.authGetUser).toHaveBeenCalledWith('query-token'); + }); +});