From 17152144abea083ec4052d6d808c493370b3fcc3 Mon Sep 17 00:00:00 2001 From: phucnguyen1707 Date: Mon, 15 Jun 2026 11:33:42 +0700 Subject: [PATCH] Verify avatar auth tokens --- src/app/api/auth/upload-avatar/route.js | 103 +++++-------------- src/app/api/auth/upload-avatar/route.test.js | 65 ++++++++++++ 2 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 src/app/api/auth/upload-avatar/route.test.js diff --git a/src/app/api/auth/upload-avatar/route.js b/src/app/api/auth/upload-avatar/route.js index 41c8b79..8e0bf85 100644 --- a/src/app/api/auth/upload-avatar/route.js +++ b/src/app/api/auth/upload-avatar/route.js @@ -1,44 +1,29 @@ import { NextResponse } from 'next/server'; import { createClient } from '@supabase/supabase-js'; + +const supabaseAuth = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY); + +async function authenticateBearerToken(request) { + const authHeader = request.headers.get('authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return { error: 'Authentication required' }; + } + + const token = authHeader.substring(7); + const { data: { user }, error } = await supabaseAuth.auth.getUser(token); + + if (error || !user?.id) { + return { error: 'Invalid authentication token' }; + } + + return { user }; +} + export async function POST(request) { try { - // Get the authorization header - const authHeader = request.headers.get('authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); - } - - const token = authHeader.replace('Bearer ', ''); - - // Decode JWT token to get user ID (simple validation) - let user; - try { - // JWT tokens have 3 parts separated by dots: header.payload.signature - const tokenParts = token.split('.'); - if (tokenParts.length !== 3) { - throw new Error('Invalid token format'); - } - - // Decode the payload (second part) - const payload = JSON.parse(atob(tokenParts[1])); - - // Check if token is expired - if (payload.exp && payload.exp < Date.now() / 1000) { - throw new Error('Token expired'); - } - - // Extract user info from payload - user = { - id: payload.sub, - email: payload.email - }; - - if (!user.id) { - throw new Error('Invalid token payload'); - } - } catch (error) { - console.error('Token validation error:', error); - return NextResponse.json({ error: 'Invalid authentication token' }, { status: 401 }); + const { user, error: authError } = await authenticateBearerToken(request); + if (authError || !user) { + return NextResponse.json({ error: authError }, { status: 401 }); } // Create service role client for database operations @@ -74,7 +59,7 @@ export async function POST(request) { const fileBuffer = await file.arrayBuffer(); // Upload to Supabase Storage - const { data: uploadData, error: uploadError } = await supabase.storage + const { error: uploadError } = await supabase.storage .from('avatars') .upload(fileName, fileBuffer, { contentType: file.type, @@ -119,43 +104,9 @@ export async function POST(request) { export async function DELETE(request) { try { - // Get the authorization header - const authHeader = request.headers.get('authorization'); - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return NextResponse.json({ error: 'Authentication required' }, { status: 401 }); - } - - const token = authHeader.replace('Bearer ', ''); - - // Decode JWT token to get user ID (simple validation) - let user; - try { - // JWT tokens have 3 parts separated by dots: header.payload.signature - const tokenParts = token.split('.'); - if (tokenParts.length !== 3) { - throw new Error('Invalid token format'); - } - - // Decode the payload (second part) - const payload = JSON.parse(atob(tokenParts[1])); - - // Check if token is expired - if (payload.exp && payload.exp < Date.now() / 1000) { - throw new Error('Token expired'); - } - - // Extract user info from payload - user = { - id: payload.sub, - email: payload.email - }; - - if (!user.id) { - throw new Error('Invalid token payload'); - } - } catch (error) { - console.error('Token validation error:', error); - return NextResponse.json({ error: 'Invalid authentication token' }, { status: 401 }); + const { user, error: authError } = await authenticateBearerToken(request); + if (authError || !user) { + return NextResponse.json({ error: authError }, { status: 401 }); } // Create service role client for storage and database operations @@ -184,4 +135,4 @@ export async function DELETE(request) { console.error('Avatar removal error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/auth/upload-avatar/route.test.js b/src/app/api/auth/upload-avatar/route.test.js new file mode 100644 index 0000000..45b458b --- /dev/null +++ b/src/app/api/auth/upload-avatar/route.test.js @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + createClient: vi.fn(() => ({ + auth: { + getUser: mocks.authGetUser + } + })) +})); + +vi.mock('@supabase/supabase-js', () => ({ + createClient: mocks.createClient +})); + +const forgedToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ2aWN0aW0tdXNlciJ9.fake-signature'; + +function avatarRequest(method) { + return new Request('https://example.com/api/auth/upload-avatar', { + method, + headers: { + authorization: `Bearer ${forgedToken}` + } + }); +} + +describe('upload-avatar 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-role-key'; + mocks.authGetUser.mockResolvedValue({ + data: { user: null }, + error: { message: 'invalid signature' } + }); + }); + + it('rejects forged bearer tokens before POST storage/database work', async () => { + const { POST } = await import('./route.js'); + + const response = await POST(avatarRequest('POST')); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body.error).toBe('Invalid authentication token'); + expect(mocks.authGetUser).toHaveBeenCalledWith(forgedToken); + expect(mocks.createClient).toHaveBeenCalledTimes(1); + expect(mocks.createClient).toHaveBeenCalledWith('https://example.supabase.co', 'anon-key'); + }); + + it('rejects forged bearer tokens before DELETE database work', async () => { + const { DELETE } = await import('./route.js'); + + const response = await DELETE(avatarRequest('DELETE')); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body.error).toBe('Invalid authentication token'); + expect(mocks.authGetUser).toHaveBeenCalledWith(forgedToken); + expect(mocks.createClient).toHaveBeenCalledTimes(1); + expect(mocks.createClient).toHaveBeenCalledWith('https://example.supabase.co', 'anon-key'); + }); +});