From 17d963b2a6d6d5d9fb900f70b00cf1d6198c4b02 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Sat, 11 Jul 2026 10:30:18 -0600 Subject: [PATCH] Parse auth cookies without requiring spaces --- src/app/api/auth/backup-pin/route.js | 2 +- src/app/api/auth/backup-pin/route.test.js | 72 +++++++++++++++++++ src/app/api/auth/salt/route.js | 2 +- src/app/api/auth/salt/route.test.js | 72 +++++++++++++++++++ .../conversations/[id]/participants/route.js | 2 +- src/app/api/chat/messages/route.js | 2 +- .../disappearing-messages/presets/route.js | 4 +- .../[id]/disappearing-messages/route.js | 2 +- src/app/api/user/nuclear-delete/route.js | 4 +- 9 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 src/app/api/auth/backup-pin/route.test.js create mode 100644 src/app/api/auth/salt/route.test.js diff --git a/src/app/api/auth/backup-pin/route.js b/src/app/api/auth/backup-pin/route.js index 57e41de..40ce432 100644 --- a/src/app/api/auth/backup-pin/route.js +++ b/src/app/api/auth/backup-pin/route.js @@ -39,7 +39,7 @@ async function authenticateUser(request) { } const cookies = Object.fromEntries( - cookieHeader.split('; ').map(cookie => { + cookieHeader.split(/;\s*/).map(cookie => { const [name, value] = cookie.split('='); 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 new file mode 100644 index 0000000..3345a21 --- /dev/null +++ b/src/app/api/auth/backup-pin/route.test.js @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + serviceFrom: vi.fn() +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(() => ({ + auth: { + getUser: mocks.authGetUser + } + })) +})); + +vi.mock('@/lib/supabase/service-role.js', () => ({ + createServiceRoleClient: vi.fn(() => ({ + from: mocks.serviceFrom + })) +})); + +function cookieValue(token) { + return `base64-${Buffer.from(JSON.stringify({ access_token: token })).toString('base64')}`; +} + +function createUsersQuery() { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + single: vi.fn(() => + Promise.resolve({ + data: { backup_pin_hash: 'pin-hash' }, + error: null + }) + ) + }; + return query; +} + +describe('backup PIN 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'; + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.serviceFrom.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + throw new Error(`Unexpected table: ${table}`); + }); + }); + + it('accepts valid cookie headers without a space after semicolons', 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=${cookieValue('access-token')};session=ignored` + } + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ hasPin: true }); + expect(mocks.authGetUser).toHaveBeenCalledWith('access-token'); + }); +}); diff --git a/src/app/api/auth/salt/route.js b/src/app/api/auth/salt/route.js index b445e91..540fc37 100644 --- a/src/app/api/auth/salt/route.js +++ b/src/app/api/auth/salt/route.js @@ -28,7 +28,7 @@ async function authenticateUser(request) { // Fall back to cookies const cookieHeader = request.headers.get('cookie') ?? ''; const cookies = Object.fromEntries( - cookieHeader.split('; ').map(c => { + cookieHeader.split(/;\s*/).map(c => { const [name, ...rest] = c.split('='); return [name, rest.join('=')]; }) diff --git a/src/app/api/auth/salt/route.test.js b/src/app/api/auth/salt/route.test.js new file mode 100644 index 0000000..4495508 --- /dev/null +++ b/src/app/api/auth/salt/route.test.js @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + serviceFrom: vi.fn() +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: vi.fn(() => ({ + auth: { + getUser: mocks.authGetUser + } + })) +})); + +vi.mock('@/lib/supabase/service-role.js', () => ({ + createServiceRoleClient: vi.fn(() => ({ + from: mocks.serviceFrom + })) +})); + +function cookieValue(token) { + return `base64-${Buffer.from(JSON.stringify({ access_token: token })).toString('base64')}`; +} + +function createUsersQuery() { + const query = { + select: vi.fn(() => query), + eq: vi.fn(() => query), + single: vi.fn(() => + Promise.resolve({ + data: { salt: 'stored-salt' }, + error: null + }) + ) + }; + return query; +} + +describe('salt 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'; + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.serviceFrom.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + throw new Error(`Unexpected table: ${table}`); + }); + }); + + it('accepts valid cookie headers without a space after semicolons', async () => { + const { GET } = await import('./route.js'); + const response = await GET( + new Request('https://qrypt.chat/api/auth/salt', { + headers: { + cookie: `sb-xydzwxwsbgmznthiiscl-auth-token=${cookieValue('access-token')};session=ignored` + } + }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ salt: 'stored-salt' }); + expect(mocks.authGetUser).toHaveBeenCalledWith('access-token'); + }); +}); diff --git a/src/app/api/chat/conversations/[id]/participants/route.js b/src/app/api/chat/conversations/[id]/participants/route.js index 436a708..5c439a9 100644 --- a/src/app/api/chat/conversations/[id]/participants/route.js +++ b/src/app/api/chat/conversations/[id]/participants/route.js @@ -36,7 +36,7 @@ async function authenticateUser(request) { // Parse cookies to find auth token const cookies = Object.fromEntries( - cookieHeader.split('; ').map(cookie => { + cookieHeader.split(/;\s*/).map(cookie => { const [name, value] = cookie.split('='); return [name, decodeURIComponent(value)]; }) diff --git a/src/app/api/chat/messages/route.js b/src/app/api/chat/messages/route.js index 6830b14..d81cce1 100644 --- a/src/app/api/chat/messages/route.js +++ b/src/app/api/chat/messages/route.js @@ -38,7 +38,7 @@ async function authenticateUser(request) { // Extract access token from cookies const cookies = Object.fromEntries( - cookieHeader.split('; ').map(cookie => { + cookieHeader.split(/;\s*/).map(cookie => { const [name, ...rest] = cookie.split('='); return [name, rest.join('=')]; }) diff --git a/src/app/api/conversations/[id]/disappearing-messages/presets/route.js b/src/app/api/conversations/[id]/disappearing-messages/presets/route.js index 1001e51..08f2fd2 100644 --- a/src/app/api/conversations/[id]/disappearing-messages/presets/route.js +++ b/src/app/api/conversations/[id]/disappearing-messages/presets/route.js @@ -23,7 +23,7 @@ async function authenticateUser(request) { // Extract access token from cookies const cookies = Object.fromEntries( - cookieHeader.split('; ').map(cookie => { + cookieHeader.split(/;\s*/).map(cookie => { const [name, ...rest] = cookie.split('='); return [name, rest.join('=')]; }) @@ -101,4 +101,4 @@ export async function GET(request, { params } = {}) { console.error('Error in GET /api/conversations/:id/disappearing-messages/presets:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/conversations/[id]/disappearing-messages/route.js b/src/app/api/conversations/[id]/disappearing-messages/route.js index 3ab243f..29be783 100644 --- a/src/app/api/conversations/[id]/disappearing-messages/route.js +++ b/src/app/api/conversations/[id]/disappearing-messages/route.js @@ -36,7 +36,7 @@ async function authenticateUser(request) { // Extract access token from cookies const cookies = Object.fromEntries( - cookieHeader.split('; ').map(cookie => { + cookieHeader.split(/;\s*/).map(cookie => { const [name, ...rest] = cookie.split('='); return [name, rest.join('=')]; }) diff --git a/src/app/api/user/nuclear-delete/route.js b/src/app/api/user/nuclear-delete/route.js index de16067..f836edf 100644 --- a/src/app/api/user/nuclear-delete/route.js +++ b/src/app/api/user/nuclear-delete/route.js @@ -29,7 +29,7 @@ async function authenticateUser(request) { // Extract access token from cookies const cookies = Object.fromEntries( - cookieHeader.split('; ').map(cookie => { + cookieHeader.split(/;\s*/).map(cookie => { const [name, ...rest] = cookie.split('='); return [name, rest.join('=')]; }) @@ -198,4 +198,4 @@ export async function PUT() { export async function PATCH() { return NextResponse.json({ error: 'Method not allowed' }, { status: 405 }); -} \ No newline at end of file +}