diff --git a/src/app/api/auth/verify-sms/route.js b/src/app/api/auth/verify-sms/route.js index 3cb90c61..abe732b1 100644 --- a/src/app/api/auth/verify-sms/route.js +++ b/src/app/api/auth/verify-sms/route.js @@ -19,6 +19,19 @@ function isValidPhoneNumber(phoneNumber) { return phoneRegex.test(phoneNumber); } +/** + * @param {string | null} authHeader + * @returns {string | null} + */ +function getBearerToken(authHeader) { + if (typeof authHeader !== 'string') return null; + + const match = authHeader.match(/^Bearer\s+(.+)$/i); + const token = match?.[1]?.trim(); + + return token || null; +} + /** * POST /api/auth/verify-sms * Verify SMS code and create/login user @@ -59,7 +72,16 @@ export async function POST(request, { params } = {}) { }); // Validate input based on request type - if (useSession && authHeader) { + const token = getBearerToken(authHeader); + if (useSession) { + if (!token) { + logger.error('Missing session authorization header'); + return NextResponse.json( + { error: 'Missing or invalid authorization header', code: 'SESSION_AUTH_REQUIRED' }, + { status: 401 } + ); + } + // Session-based request - username required if (!phoneNumber || !username) { logger.error( 'Missing required fields for session-based request', { phoneNumber: !!phoneNumber, username: !!username }); @@ -97,11 +119,9 @@ export async function POST(request, { params } = {}) { let verifyData; let verifyError; - if (useSession && authHeader) { + if (useSession) { logger.info('Processing session-based profile completion'); - - const token = authHeader.replace('Bearer ', ''); - + try { // Validate the JWT token by getting user info const { data: { user }, error: userError } = await supabase.auth.getUser(token); @@ -462,4 +482,4 @@ export async function POST(request, { params } = {}) { { status: 500 } ); } -} \ No newline at end of file +} diff --git a/src/app/api/auth/verify-sms/route.test.js b/src/app/api/auth/verify-sms/route.test.js index 4e52fe21..50c3cd2e 100644 --- a/src/app/api/auth/verify-sms/route.test.js +++ b/src/app/api/auth/verify-sms/route.test.js @@ -16,11 +16,11 @@ vi.mock('@supabase/supabase-js', () => ({ createClient: mocks.createClient })); -function sessionRequest(body) { +function sessionRequest(body, authorization = 'Bearer test-session-token') { return new Request('https://example.com/api/auth/verify-sms', { method: 'POST', headers: { - authorization: 'Bearer test-session-token', + authorization, 'content-type': 'application/json' }, body: JSON.stringify(body) @@ -59,6 +59,37 @@ describe('verify-sms session phone binding', () => { expect(mocks.getUser).toHaveBeenCalledWith('test-session-token'); }); + it('normalizes bearer scheme casing and extra spaces for session signup', async () => { + mocks.getUser.mockResolvedValue({ + data: { user: { id: 'user-1', phone: '+15559999999' } }, + error: null + }); + + const { POST } = await import('./route.js'); + const res = await POST(sessionRequest({ + useSession: true, + phoneNumber: '+15559999999', + username: 'alice' + }, 'bearer test-session-token ')); + + expect(mocks.getUser).toHaveBeenCalledWith('test-session-token'); + expect(res.status).not.toBe(401); + }); + + it('rejects an empty session bearer header before session validation', async () => { + const { POST } = await import('./route.js'); + const res = await POST(sessionRequest({ + useSession: true, + phoneNumber: '+15559999999', + username: 'alice' + }, 'Bearer ')); + const body = await res.json(); + + expect(res.status).toBe(401); + expect(body.code).toBe('SESSION_AUTH_REQUIRED'); + expect(mocks.getUser).not.toHaveBeenCalled(); + }); + it('rejects anonymous / phoneless sessions', async () => { mocks.getUser.mockResolvedValue({ data: { user: { id: 'anon', phone: null } },