diff --git a/src/app/api/auth/backup-pin/route.js b/src/app/api/auth/backup-pin/route.js index 23d704f..4f2e901 100644 --- a/src/app/api/auth/backup-pin/route.js +++ b/src/app/api/auth/backup-pin/route.js @@ -49,7 +49,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/auth/backup-pin/route.test.js b/src/app/api/auth/backup-pin/route.test.js index dfdf9d0..6407f06 100644 --- a/src/app/api/auth/backup-pin/route.test.js +++ b/src/app/api/auth/backup-pin/route.test.js @@ -23,6 +23,10 @@ function cookieValue(token) { return `base64-${Buffer.from(JSON.stringify({ access_token: token })).toString('base64')}`; } +function paddedCookieValue() { + return 'base64-eyJhY2Nlc3NfdG9rZW4iOiJhYmMiLCJwYWRkaW5nIjoieCJ9=='; +} + function createUsersQuery() { const query = { select: vi.fn(() => query), @@ -70,6 +74,20 @@ describe('backup PIN 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/auth/backup-pin', { + headers: { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${paddedCookieValue()}` + } + }) + ); + + expect(response.status).toBe(200); + expect(mocks.authGetUser).toHaveBeenCalledWith('abc'); + }); + it('normalizes bearer scheme casing and extra spaces', async () => { const { GET } = await import('./route.js'); const response = await GET( diff --git a/src/app/api/auth/key-backup/route.js b/src/app/api/auth/key-backup/route.js index 85c1c75..18aa950 100644 --- a/src/app/api/auth/key-backup/route.js +++ b/src/app/api/auth/key-backup/route.js @@ -51,7 +51,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/auth/key-backup/route.test.js b/src/app/api/auth/key-backup/route.test.js index 55ba430..219dec9 100644 --- a/src/app/api/auth/key-backup/route.test.js +++ b/src/app/api/auth/key-backup/route.test.js @@ -23,6 +23,10 @@ function cookieValue(token) { return `base64-${Buffer.from(JSON.stringify({ access_token: token })).toString('base64')}`; } +function paddedCookieValue() { + return 'base64-eyJhY2Nlc3NfdG9rZW4iOiJhYmMiLCJwYWRkaW5nIjoieCJ9=='; +} + function createBackupQuery() { const query = { select: vi.fn(() => query), @@ -98,6 +102,20 @@ describe('key backup 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/auth/key-backup', { + headers: { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${paddedCookieValue()}` + } + }) + ); + + expect(response.status).toBe(200); + expect(mocks.authGetUser).toHaveBeenCalledWith('abc'); + }); + it('normalizes bearer scheme casing and extra spaces', async () => { const { GET } = await import('./route.js'); const response = await GET( diff --git a/src/app/api/chat/conversations/[id]/participants/route.js b/src/app/api/chat/conversations/[id]/participants/route.js index 0292fcd..e1c1afa 100644 --- a/src/app/api/chat/conversations/[id]/participants/route.js +++ b/src/app/api/chat/conversations/[id]/participants/route.js @@ -37,7 +37,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/chat/conversations/[id]/participants/route.test.js b/src/app/api/chat/conversations/[id]/participants/route.test.js index 338be74..c8d0da2 100644 --- a/src/app/api/chat/conversations/[id]/participants/route.test.js +++ b/src/app/api/chat/conversations/[id]/participants/route.test.js @@ -21,6 +21,10 @@ vi.mock('@/lib/supabase/service-role.js', () => ({ })) })); +function paddedCookieValue() { + return 'base64-eyJhY2Nlc3NfdG9rZW4iOiJhYmMiLCJwYWRkaW5nIjoieCJ9=='; +} + function createUsersQuery() { const query = { select: vi.fn(() => query), @@ -110,6 +114,27 @@ describe('GET /api/chat/conversations/[id]/participants', () => { expect(mocks.participantEq).toHaveBeenCalledWith('conversation_id', 'conversation-1'); }); + it('preserves equals padding in base64 auth cookies', async () => { + mocks.serviceFrom.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + if (table === 'conversation_participants') { + return createParticipantCheckQuery(); + } + throw new Error(`Unexpected table: ${table}`); + }); + + const { GET } = await import('./route.js'); + const response = await GET( + new Request('https://qrypt.chat/api/chat/conversations/conversation-1/participants', { + headers: { cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${paddedCookieValue()}` } + }), + { params: Promise.resolve({ id: 'conversation-1' }) } + ); + + expect(response.status).toBe(200); + expect(mocks.authGetUser).toHaveBeenCalledWith('abc'); + }); + it('rejects missing async route params after authentication', async () => { mocks.serviceFrom.mockImplementation((table) => { if (table === 'users') return createUsersQuery();