fix: enforce API token scope on requests
- Add apiTokenScope to App.Locals type definition - Store token scope in event.locals during API token auth - Block write operations (POST/PATCH/PUT/DELETE) for read-scoped tokens - Block admin paths for non-admin-scoped tokens - Returns 403 with descriptive error message
This commit is contained in:
Vendored
+2
@@ -18,6 +18,8 @@ declare global {
|
|||||||
id: string;
|
id: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
} | null;
|
} | null;
|
||||||
|
/** API token scope — set when auth is via Bearer token, null for JWT sessions */
|
||||||
|
apiTokenScope: 'read' | 'write' | 'admin' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageData {
|
interface PageData {
|
||||||
|
|||||||
+25
-1
@@ -27,6 +27,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
// Initialize locals
|
// Initialize locals
|
||||||
event.locals.user = null;
|
event.locals.user = null;
|
||||||
event.locals.session = null;
|
event.locals.session = null;
|
||||||
|
event.locals.apiTokenScope = null;
|
||||||
|
|
||||||
const accessToken = event.cookies.get(ACCESS_TOKEN_COOKIE);
|
const accessToken = event.cookies.get(ACCESS_TOKEN_COOKIE);
|
||||||
const refreshToken = event.cookies.get(REFRESH_TOKEN_COOKIE);
|
const refreshToken = event.cookies.get(REFRESH_TOKEN_COOKIE);
|
||||||
@@ -111,6 +112,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
expiresAt: new Date(Date.now() + 15 * 60 * 1000)
|
expiresAt: new Date(Date.now() + 15 * 60 * 1000)
|
||||||
};
|
};
|
||||||
|
event.locals.apiTokenScope = tokenResult.scope as 'read' | 'write' | 'admin';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// API token validation failed — continue as unauthenticated
|
// API token validation failed — continue as unauthenticated
|
||||||
@@ -118,9 +120,31 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route protection
|
|
||||||
const { pathname } = event.url;
|
const { pathname } = event.url;
|
||||||
|
|
||||||
|
// API token scope enforcement
|
||||||
|
if (event.locals.apiTokenScope) {
|
||||||
|
const method = event.request.method;
|
||||||
|
const scope = event.locals.apiTokenScope;
|
||||||
|
const isWriteMethod = method === 'POST' || method === 'PATCH' || method === 'PUT' || method === 'DELETE';
|
||||||
|
const isAdminPath = pathname.startsWith('/api/admin') || pathname.startsWith('/admin');
|
||||||
|
|
||||||
|
if (scope === 'read' && isWriteMethod) {
|
||||||
|
return new Response(JSON.stringify({ success: false, data: null, error: 'API token scope "read" does not allow write operations' }), {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (scope !== 'admin' && isAdminPath) {
|
||||||
|
return new Response(JSON.stringify({ success: false, data: null, error: 'API token scope does not allow admin operations' }), {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route protection
|
||||||
|
|
||||||
if (!event.locals.user && !isPublicPath(pathname)) {
|
if (!event.locals.user && !isPublicPath(pathname)) {
|
||||||
// Check if this is a guest-accessible board route
|
// Check if this is a guest-accessible board route
|
||||||
const boardMatch = pathname.match(/^\/boards\/([^/]+)/);
|
const boardMatch = pathname.match(/^\/boards\/([^/]+)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user