diff --git a/src/app/api/crypto/public-keys/all/route.js b/src/app/api/crypto/public-keys/all/route.js index 5cb98470..cbede1ee 100644 --- a/src/app/api/crypto/public-keys/all/route.js +++ b/src/app/api/crypto/public-keys/all/route.js @@ -23,7 +23,9 @@ async function authenticateUser(request) { const cookies = Object.fromEntries( cookieHeader.split(/;\s*/).map((cookie) => { - const [name, value] = cookie.split('='); + const separator = cookie.indexOf('='); + const name = separator === -1 ? cookie : cookie.slice(0, separator); + const value = separator === -1 ? '' : cookie.slice(separator + 1); return [name, decodeURIComponent(value)]; }) ); diff --git a/src/app/api/crypto/public-keys/all/route.test.js b/src/app/api/crypto/public-keys/all/route.test.js new file mode 100644 index 00000000..93de6c82 --- /dev/null +++ b/src/app/api/crypto/public-keys/all/route.test.js @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + from: vi.fn(), +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(() => ({ + auth: { getUser: mocks.authGetUser }, + from: mocks.from, + })), +})); + +describe('all public keys cookie authentication', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + process.env.NEXT_PUBLIC_SUPABASE_URL = 'https://example.supabase.co'; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = 'anon-key'; + process.env.SUPABASE_SERVICE_ROLE_KEY = 'service-key'; + mocks.authGetUser.mockResolvedValue({ data: { user: { id: 'auth-user-id' } }, error: null }); + mocks.from.mockReturnValue({ + select: vi.fn(() => ({ + not: vi.fn(() => Promise.resolve({ data: [{ user_id: 'user-1', public_key: 'key-1' }], error: null })), + })), + }); + }); + + it('preserves equals padding in base64 auth cookies', async () => { + const { GET } = await import('./route.js'); + const response = await GET( + new Request('https://qrypt.chat/api/crypto/public-keys/all', { + headers: { + cookie: 'sb-xydzwxwsbgmznthiiscl-auth-token=base64-eyJhY2Nlc3NfdG9rZW4iOiJhYmMiLCJwYWRkaW5nIjoieCJ9==', + }, + }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual([{ user_id: 'user-1', public_key: 'key-1' }]); + expect(mocks.authGetUser).toHaveBeenCalledWith('abc'); + }); +}); diff --git a/src/app/api/crypto/public-keys/route.js b/src/app/api/crypto/public-keys/route.js index 29166cbe..adc35c8b 100644 --- a/src/app/api/crypto/public-keys/route.js +++ b/src/app/api/crypto/public-keys/route.js @@ -33,7 +33,9 @@ async function authenticateUser(request) { // Parse cookies to find auth token const cookies = Object.fromEntries( cookieHeader.split(/;\s*/).map(cookie => { - const [name, value] = cookie.split('='); + const separator = cookie.indexOf('='); + const name = separator === -1 ? cookie : cookie.slice(0, separator); + const value = separator === -1 ? '' : cookie.slice(separator + 1); return [name, decodeURIComponent(value)]; }) ); diff --git a/src/app/api/crypto/public-keys/route.test.js b/src/app/api/crypto/public-keys/route.test.js index 9e021418..74987a85 100644 --- a/src/app/api/crypto/public-keys/route.test.js +++ b/src/app/api/crypto/public-keys/route.test.js @@ -25,6 +25,10 @@ function cookieValue(token) { return `base64-${Buffer.from(JSON.stringify({ access_token: token })).toString('base64')}`; } +function paddedCookieValue() { + return 'base64-eyJhY2Nlc3NfdG9rZW4iOiJhYmMiLCJwYWRkaW5nIjoieCJ9=='; +} + function createUsersQuery() { let selected = ''; const query = { @@ -83,6 +87,20 @@ describe('public key cookie authentication', () => { expect(mocks.authGetUser).toHaveBeenCalledWith('access-token'); }); + it('preserves equals padding in base64 auth cookies', async () => { + const { GET } = await import('./route.js'); + const response = await GET( + new Request('https://qrypt.chat/api/crypto/public-keys?user_id=target-user-id', { + headers: { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${paddedCookieValue()}` + } + }) + ); + + expect(response.status).toBe(200); + expect(mocks.authGetUser).toHaveBeenCalledWith('abc'); + }); + it('rejects non-string public keys before upsert RPC work', async () => { const { PUT } = await import('./route.js'); const response = await PUT(