Skip to content

Commit 3f608b9

Browse files
ralyodioclaude
andauthored
Accept Bearer tokens on the seven cookie-only API routes (0.5.3) (#286)
qc send failed with 'Unauthorized': it reads participants from /api/chat/conversations/[id]/participants and public keys from /api/crypto/public-keys, and both parsed only cookies. Same for chat/messages, chat/messages/[id], public-keys/all and the disappearing-messages routes. They now take Authorization: Bearer first (shared bearerToken()), cookies after, which also covers cookie-less passkey and CoinPay browser sessions. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f0d0944 commit 3f608b9

11 files changed

Lines changed: 97 additions & 25 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "qryptchat-web",
33
"private": true,
4-
"version": "0.5.2",
4+
"version": "0.5.3",
55
"type": "module",
66
"bin": {
77
"qc": "./bin/qc.js",

‎src/app/api/chat/conversations/[id]/participants/route.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NextResponse } from 'next/server';
55
import { createClient } from '@supabase/supabase-js';
66
import { supabaseAuthCookieName } from '@/lib/supabase/auth-cookie.js';
77
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
8+
import { bearerToken } from '@/lib/auth/bearer.js';
89

910
// Lazy service role client creation
1011
let supabaseServiceRole = null;
@@ -30,8 +31,10 @@ async function resolveRouteParams(params) {
3031
async function authenticateUser(request) {
3132
try {
3233
// Get access token from cookies
33-
const cookieHeader = request.headers.get('cookie');
34-
if (!cookieHeader) {
34+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
35+
const bearer = bearerToken(request);
36+
const cookieHeader = request.headers.get('cookie') || '';
37+
if (!bearer && !cookieHeader) {
3538
return { error: 'No cookies found' };
3639
}
3740

@@ -46,12 +49,12 @@ async function authenticateUser(request) {
4649
);
4750

4851
// Try different cookie sources for the access token
49-
let accessToken = null;
52+
let accessToken = bearer;
5053

5154
console.log('🔐 Available cookies:', Object.keys(cookies));
5255

5356
// Try Supabase-specific cookies first
54-
if (cookies[supabaseAuthCookieName()]) {
57+
if (!accessToken && cookies[supabaseAuthCookieName()]) {
5558
console.log('🔐 Found Supabase auth token cookie');
5659
try {
5760
let tokenData = cookies[supabaseAuthCookieName()];

‎src/app/api/chat/conversations/[id]/participants/route.test.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ describe('GET /api/chat/conversations/[id]/participants', () => {
115115
expect(mocks.participantEq).toHaveBeenCalledWith('conversation_id', 'conversation-1');
116116
});
117117

118+
it('accepts a Bearer token with no cookie at all (qc, MCP, passkey sessions)', async () => {
119+
mocks.serviceFrom.mockImplementation((table) => {
120+
if (table === 'users') return createUsersQuery();
121+
if (table === 'conversation_participants') return createParticipantCheckQuery();
122+
throw new Error(`Unexpected table: ${table}`);
123+
});
124+
125+
const { GET } = await import('./route.js');
126+
const response = await GET(
127+
new Request('https://qrypt.chat/api/chat/conversations/conversation-1/participants', {
128+
headers: { authorization: 'Bearer qc-token' }
129+
}),
130+
{ params: Promise.resolve({ id: 'conversation-1' }) }
131+
);
132+
133+
expect(response.status).toBe(200);
134+
expect(mocks.authGetUser).toHaveBeenCalledWith('qc-token');
135+
});
136+
118137
it('preserves equals padding in base64 auth cookies', async () => {
119138
mocks.serviceFrom.mockImplementation((table) => {
120139
if (table === 'users') return createUsersQuery();

‎src/app/api/chat/messages/[id]/route.js‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { NextResponse } from 'next/server';
66
import { createClient } from '@supabase/supabase-js';
7+
import { bearerToken } from '@/lib/auth/bearer.js';
78

89
async function resolveRouteParams(params) {
910
return (await params) || {};
@@ -24,12 +25,14 @@ async function authenticateUser(request) {
2425
.split(';')
2526
.find(c => c.trim().startsWith('sb-access-token='));
2627

27-
if (!sessionCookie) {
28+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
29+
const bearer = bearerToken(request);
30+
if (!bearer && !sessionCookie) {
2831
return null;
2932
}
3033

31-
const separatorIndex = sessionCookie.indexOf('=');
32-
const token = separatorIndex >= 0 ? sessionCookie.slice(separatorIndex + 1) : '';
34+
const separatorIndex = sessionCookie ? sessionCookie.indexOf('=') : -1;
35+
const token = bearer || (separatorIndex >= 0 ? sessionCookie.slice(separatorIndex + 1) : '');
3336
if (!token) {
3437
return null;
3538
}

‎src/app/api/chat/messages/route.js‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NextResponse } from 'next/server';
66
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
77
import { createClient } from '@supabase/supabase-js';
88
import { normalizeMessagePagination } from './pagination.js';
9+
import { bearerToken } from '@/lib/auth/bearer.js';
910
// Lazy service role client creation
1011
let supabaseServiceRole = null;
1112
function getServiceRoleClient() {
@@ -30,8 +31,10 @@ async function resolveRouteParams(params) {
3031
async function authenticateUser(request) {
3132
try {
3233
// Parse cookies to get JWT token
33-
const cookieHeader = request.headers.get('cookie');
34-
if (!cookieHeader) {
34+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
35+
const bearer = bearerToken(request);
36+
const cookieHeader = request.headers.get('cookie') || '';
37+
if (!bearer && !cookieHeader) {
3538
console.log('🔐 [API] ❌ No cookies found in request');
3639
return { user: null, error: 'No authentication cookies found' };
3740
}
@@ -44,7 +47,7 @@ async function authenticateUser(request) {
4447
})
4548
);
4649

47-
const accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];
50+
const accessToken = bearer || cookies['sb-access-token'] || cookies['sb-refresh-token'];
4851
if (!accessToken) {
4952
console.log('🔐 [API] ❌ No Supabase auth tokens found in cookies');
5053
return { user: null, error: 'No authentication tokens found' };

‎src/app/api/conversations/[id]/disappearing-messages/presets/route.js‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { NextResponse } from 'next/server';
55
import { createClient } from '@supabase/supabase-js';
6+
import { bearerToken } from '@/lib/auth/bearer.js';
67

78
// Create regular Supabase client for authentication
89
const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
@@ -15,8 +16,10 @@ const supabaseClient = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, proces
1516
async function authenticateUser(request) {
1617
try {
1718
// Parse cookies to get JWT token
18-
const cookieHeader = request.headers.get('cookie');
19-
if (!cookieHeader) {
19+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
20+
const bearer = bearerToken(request);
21+
const cookieHeader = request.headers.get('cookie') || '';
22+
if (!bearer && !cookieHeader) {
2023
console.log('🔐 [API] ❌ No cookies found in request');
2124
return { user: null, error: 'No authentication cookies found' };
2225
}
@@ -29,7 +32,7 @@ async function authenticateUser(request) {
2932
})
3033
);
3134

32-
const accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];
35+
const accessToken = bearer || cookies['sb-access-token'] || cookies['sb-refresh-token'];
3336
if (!accessToken) {
3437
console.log('🔐 [API] ❌ No Supabase auth tokens found in cookies');
3538
return { user: null, error: 'No authentication tokens found' };

‎src/app/api/conversations/[id]/disappearing-messages/route.js‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { NextResponse } from 'next/server';
55
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
66
import { createClient } from '@supabase/supabase-js';
7+
import { bearerToken } from '@/lib/auth/bearer.js';
78
// Lazy service role client creation
89
let supabaseServiceRole = null;
910
function getServiceRoleClient() {
@@ -28,8 +29,10 @@ async function resolveRouteParams(params) {
2829
async function authenticateUser(request) {
2930
try {
3031
// Parse cookies to get JWT token
31-
const cookieHeader = request.headers.get('cookie');
32-
if (!cookieHeader) {
32+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
33+
const bearer = bearerToken(request);
34+
const cookieHeader = request.headers.get('cookie') || '';
35+
if (!bearer && !cookieHeader) {
3336
console.log('🔐 [API] ❌ No cookies found in request');
3437
return { user: null, error: 'No authentication cookies found' };
3538
}
@@ -42,7 +45,7 @@ async function authenticateUser(request) {
4245
})
4346
);
4447

45-
const accessToken = cookies['sb-access-token'] || cookies['sb-refresh-token'];
48+
const accessToken = bearer || cookies['sb-access-token'] || cookies['sb-refresh-token'];
4649
if (!accessToken) {
4750
console.log('🔐 [API] ❌ No Supabase auth tokens found in cookies');
4851
return { user: null, error: 'No authentication tokens found' };

‎src/app/api/crypto/public-keys/all/route.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse } from 'next/server';
22
import { createClient } from '@supabase/supabase-js';
33
import { supabaseAuthCookieName } from '@/lib/supabase/auth-cookie.js';
4+
import { bearerToken } from '@/lib/auth/bearer.js';
45

56
// Regular (anon) client used ONLY to validate the caller's JWT. Never use the
67
// service-role key for auth checks.
@@ -17,8 +18,10 @@ const supabaseClient = createClient(
1718
*/
1819
async function authenticateUser(request) {
1920
try {
20-
const cookieHeader = request.headers.get('cookie');
21-
if (!cookieHeader) {
21+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
22+
const bearer = bearerToken(request);
23+
const cookieHeader = request.headers.get('cookie') || '';
24+
if (!bearer && !cookieHeader) {
2225
return { error: 'No cookies found' };
2326
}
2427

@@ -31,10 +34,10 @@ async function authenticateUser(request) {
3134
})
3235
);
3336

34-
let accessToken = null;
37+
let accessToken = bearer;
3538

3639
// Supabase-specific auth cookie first
37-
if (cookies[supabaseAuthCookieName()]) {
40+
if (!accessToken && cookies[supabaseAuthCookieName()]) {
3841
try {
3942
let tokenData = cookies[supabaseAuthCookieName()];
4043
if (tokenData.startsWith('base64-')) {

‎src/app/api/crypto/public-keys/route.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NextResponse } from 'next/server';
55
import { createClient } from '@supabase/supabase-js';
66
import { supabaseAuthCookieName } from '@/lib/supabase/auth-cookie.js';
77
import { createServiceRoleClient } from '@/lib/supabase/service-role.js';
8+
import { bearerToken } from '@/lib/auth/bearer.js';
89

910
// Lazy service role client creation
1011
let supabaseServiceRole = null;
@@ -47,8 +48,10 @@ export function normalizePublicKeyUserIds(value) {
4748
async function authenticateUser(request) {
4849
try {
4950
// Get access token from cookies
50-
const cookieHeader = request.headers.get('cookie');
51-
if (!cookieHeader) {
51+
// qc, MCP and cookie-less browser sessions (passkey, CoinPay) send a Bearer token.
52+
const bearer = bearerToken(request);
53+
const cookieHeader = request.headers.get('cookie') || '';
54+
if (!bearer && !cookieHeader) {
5255
return { error: 'No cookies found' };
5356
}
5457

@@ -63,10 +66,10 @@ async function authenticateUser(request) {
6366
);
6467

6568
// Try different cookie sources for the access token
66-
let accessToken = null;
69+
let accessToken = bearer;
6770

6871
// Try Supabase-specific cookies first
69-
if (cookies[supabaseAuthCookieName()]) {
72+
if (!accessToken && cookies[supabaseAuthCookieName()]) {
7073
try {
7174
let tokenData = cookies[supabaseAuthCookieName()];
7275
if (tokenData.startsWith('base64-')) {

‎src/lib/auth/bearer.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* The access token from an `Authorization: Bearer …` header, or null.
3+
*
4+
* qc, the MCP server and API clients authenticate this way, and so do browser
5+
* sessions that never get a cookie (passkey, CoinPay, phone). Routes with
6+
* their own cookie parsing must check this first.
7+
* @param {Request} request
8+
* @returns {string | null}
9+
*/
10+
export function bearerToken(request) {
11+
const header = request?.headers?.get?.('authorization');
12+
if (typeof header !== 'string') return null;
13+
const token = header.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
14+
return token || null;
15+
}

0 commit comments

Comments
 (0)