Skip to content

Commit 1715214

Browse files
Verify avatar auth tokens
1 parent 6f1c863 commit 1715214

2 files changed

Lines changed: 92 additions & 76 deletions

File tree

src/app/api/auth/upload-avatar/route.js

Lines changed: 27 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,29 @@
11
import { NextResponse } from 'next/server';
22
import { createClient } from '@supabase/supabase-js';
3+
4+
const supabaseAuth = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
5+
6+
async function authenticateBearerToken(request) {
7+
const authHeader = request.headers.get('authorization');
8+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
9+
return { error: 'Authentication required' };
10+
}
11+
12+
const token = authHeader.substring(7);
13+
const { data: { user }, error } = await supabaseAuth.auth.getUser(token);
14+
15+
if (error || !user?.id) {
16+
return { error: 'Invalid authentication token' };
17+
}
18+
19+
return { user };
20+
}
21+
322
export async function POST(request) {
423
try {
5-
// Get the authorization header
6-
const authHeader = request.headers.get('authorization');
7-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
8-
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
9-
}
10-
11-
const token = authHeader.replace('Bearer ', '');
12-
13-
// Decode JWT token to get user ID (simple validation)
14-
let user;
15-
try {
16-
// JWT tokens have 3 parts separated by dots: header.payload.signature
17-
const tokenParts = token.split('.');
18-
if (tokenParts.length !== 3) {
19-
throw new Error('Invalid token format');
20-
}
21-
22-
// Decode the payload (second part)
23-
const payload = JSON.parse(atob(tokenParts[1]));
24-
25-
// Check if token is expired
26-
if (payload.exp && payload.exp < Date.now() / 1000) {
27-
throw new Error('Token expired');
28-
}
29-
30-
// Extract user info from payload
31-
user = {
32-
id: payload.sub,
33-
email: payload.email
34-
};
35-
36-
if (!user.id) {
37-
throw new Error('Invalid token payload');
38-
}
39-
} catch (error) {
40-
console.error('Token validation error:', error);
41-
return NextResponse.json({ error: 'Invalid authentication token' }, { status: 401 });
24+
const { user, error: authError } = await authenticateBearerToken(request);
25+
if (authError || !user) {
26+
return NextResponse.json({ error: authError }, { status: 401 });
4227
}
4328

4429
// Create service role client for database operations
@@ -74,7 +59,7 @@ export async function POST(request) {
7459
const fileBuffer = await file.arrayBuffer();
7560

7661
// Upload to Supabase Storage
77-
const { data: uploadData, error: uploadError } = await supabase.storage
62+
const { error: uploadError } = await supabase.storage
7863
.from('avatars')
7964
.upload(fileName, fileBuffer, {
8065
contentType: file.type,
@@ -119,43 +104,9 @@ export async function POST(request) {
119104

120105
export async function DELETE(request) {
121106
try {
122-
// Get the authorization header
123-
const authHeader = request.headers.get('authorization');
124-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
125-
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
126-
}
127-
128-
const token = authHeader.replace('Bearer ', '');
129-
130-
// Decode JWT token to get user ID (simple validation)
131-
let user;
132-
try {
133-
// JWT tokens have 3 parts separated by dots: header.payload.signature
134-
const tokenParts = token.split('.');
135-
if (tokenParts.length !== 3) {
136-
throw new Error('Invalid token format');
137-
}
138-
139-
// Decode the payload (second part)
140-
const payload = JSON.parse(atob(tokenParts[1]));
141-
142-
// Check if token is expired
143-
if (payload.exp && payload.exp < Date.now() / 1000) {
144-
throw new Error('Token expired');
145-
}
146-
147-
// Extract user info from payload
148-
user = {
149-
id: payload.sub,
150-
email: payload.email
151-
};
152-
153-
if (!user.id) {
154-
throw new Error('Invalid token payload');
155-
}
156-
} catch (error) {
157-
console.error('Token validation error:', error);
158-
return NextResponse.json({ error: 'Invalid authentication token' }, { status: 401 });
107+
const { user, error: authError } = await authenticateBearerToken(request);
108+
if (authError || !user) {
109+
return NextResponse.json({ error: authError }, { status: 401 });
159110
}
160111

161112
// Create service role client for storage and database operations
@@ -184,4 +135,4 @@ export async function DELETE(request) {
184135
console.error('Avatar removal error:', error);
185136
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
186137
}
187-
}
138+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
const mocks = vi.hoisted(() => ({
4+
authGetUser: vi.fn(),
5+
createClient: vi.fn(() => ({
6+
auth: {
7+
getUser: mocks.authGetUser
8+
}
9+
}))
10+
}));
11+
12+
vi.mock('@supabase/supabase-js', () => ({
13+
createClient: mocks.createClient
14+
}));
15+
16+
const forgedToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ2aWN0aW0tdXNlciJ9.fake-signature';
17+
18+
function avatarRequest(method) {
19+
return new Request('https://example.com/api/auth/upload-avatar', {
20+
method,
21+
headers: {
22+
authorization: `Bearer ${forgedToken}`
23+
}
24+
});
25+
}
26+
27+
describe('upload-avatar authentication', () => {
28+
beforeEach(() => {
29+
vi.resetModules();
30+
vi.clearAllMocks();
31+
process.env.NEXT_PUBLIC_SUPABASE_URL = 'https://example.supabase.co';
32+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = 'anon-key';
33+
process.env.SUPABASE_SERVICE_ROLE_KEY = 'service-role-key';
34+
mocks.authGetUser.mockResolvedValue({
35+
data: { user: null },
36+
error: { message: 'invalid signature' }
37+
});
38+
});
39+
40+
it('rejects forged bearer tokens before POST storage/database work', async () => {
41+
const { POST } = await import('./route.js');
42+
43+
const response = await POST(avatarRequest('POST'));
44+
const body = await response.json();
45+
46+
expect(response.status).toBe(401);
47+
expect(body.error).toBe('Invalid authentication token');
48+
expect(mocks.authGetUser).toHaveBeenCalledWith(forgedToken);
49+
expect(mocks.createClient).toHaveBeenCalledTimes(1);
50+
expect(mocks.createClient).toHaveBeenCalledWith('https://example.supabase.co', 'anon-key');
51+
});
52+
53+
it('rejects forged bearer tokens before DELETE database work', async () => {
54+
const { DELETE } = await import('./route.js');
55+
56+
const response = await DELETE(avatarRequest('DELETE'));
57+
const body = await response.json();
58+
59+
expect(response.status).toBe(401);
60+
expect(body.error).toBe('Invalid authentication token');
61+
expect(mocks.authGetUser).toHaveBeenCalledWith(forgedToken);
62+
expect(mocks.createClient).toHaveBeenCalledTimes(1);
63+
expect(mocks.createClient).toHaveBeenCalledWith('https://example.supabase.co', 'anon-key');
64+
});
65+
});

0 commit comments

Comments
 (0)