UNPKG

svelte-guardian

Version:

Batteries included authentication for SvelteKit applications.

107 lines (106 loc) 5.19 kB
import { RateLimiterFactory } from '../features/rate-limiting'; import { sequence } from '@sveltejs/kit/hooks'; import { json, redirect } from '@sveltejs/kit'; import { getEndpoints } from '../features/endpoints'; export function createMiddleware(securityConfig, adapter, logger) { const endpoints = getEndpoints(securityConfig, adapter, securityConfig.emailProvider) || {}; const rateLimitingMiddleware = async ({ event, resolve }) => { // TODO Determine rate limit configuration based on route or request type const rateLimitingConfig = securityConfig.rateLimiting; if (!rateLimitingConfig) return resolve(event); const rateLimiter = RateLimiterFactory.create(rateLimitingConfig); // Apply rate limiting const rateLimitResult = rateLimiter.limit(event, rateLimitingConfig); if (!rateLimitResult.allowed) { // Optionally log the blocked request logger.warn(`Rate limit exceeded for ${event.route?.id || 'unknown route'}`, { ip: event.getClientAddress(), user: event.locals.auth?.()?.user?.id, blockedUntil: rateLimitResult.blockedUntil }); // Return a proper rate limit exceeded response return new Response(JSON.stringify({ error: 'Too Many Requests', message: 'Rate limit exceeded', retryAfter: rateLimitResult.blockedUntil }), { status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': String(Math.ceil((rateLimitResult.blockedUntil - Date.now()) / 1000)) } }); } // Attach rate limit headers for transparency const headers = { 'X-RateLimit-Limit': String(rateLimitingConfig.maxRequests || 10), 'X-RateLimit-Remaining': String(rateLimitResult.remaining), 'X-RateLimit-Reset': String(rateLimitResult.resetTime) }; Object.entries(headers).forEach(([key, value]) => { event.setHeaders({ [key]: value }); }); return resolve(event); }; const authMiddleware = async ({ event, resolve }) => { const session = await event.locals.auth(); const routeProtection = securityConfig?.routeProtection; const userRole = session?.user?.[routeProtection?.roleKey || 'role']; const currentPath = event.url.pathname; // Check public routes first if (routeProtection?.publicRoutes) { const publicRoute = Object.entries(routeProtection?.publicRoutes).find(([route]) => route === currentPath || currentPath.startsWith(route)); if (publicRoute) { if (session?.user && publicRoute[1].redirectPath) { redirect(303, publicRoute[1].redirectPath); } if (session?.user && routeProtection?.authenticatedRedirect) { redirect(303, routeProtection?.authenticatedRedirect); } } } // Check protected routes if (routeProtection?.protectedRoutes) { const protectedRoute = Object.entries(routeProtection?.protectedRoutes).find(([route]) => route === currentPath || currentPath.startsWith(route)); if (protectedRoute) { const routeConfig = protectedRoute[1]; if (!session?.user) { redirect(303, routeConfig.redirectPath || routeProtection?.redirectPath || '/'); } if (routeConfig.authenticated && !session.user) { redirect(303, routeConfig.redirectPath || routeProtection?.redirectPath || '/'); } if (routeConfig.allowedRoles && !routeConfig.allowedRoles.includes(userRole)) { redirect(303, routeConfig.redirectPath || routeProtection?.redirectPath || '/'); } } } return resolve(event); }; const endpointsMiddleware = async ({ event, resolve }) => { const { method, url } = event.request; const path = new URL(url).pathname; const endpointKey = `${method}:${path}`; const endpoint = endpoints[endpointKey]; if (endpoint) return json(await endpoint(event)); return resolve(event); }; const securityHeadersMiddleware = async ({ event, resolve }) => { // Apply security headers based on config level const headers = { 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff' }; if (securityConfig.level === 'strict') { headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'"; } Object.entries(headers).forEach(([key, value]) => { event.setHeaders({ [key]: value }); }); return resolve(event); }; return sequence(rateLimitingMiddleware, authMiddleware, endpointsMiddleware, securityHeadersMiddleware); }