import crypto from 'crypto'; import type { RequestEvent } from '@sveltejs/kit'; import { authRateLimiter } from './rate-limiter'; import { allowlistManager } from './allowlist-manager'; // Dynamic import for SvelteKit environment let env: any = process.env; try { // Only import $env in SvelteKit context if (typeof process.env.VITE !== 'undefined') { const envModule = await import('$env/dynamic/private'); env = envModule.env; } } catch { // Fallback to process.env when not in SvelteKit env = process.env; } // Authentication configuration interface AuthConfig { enabled: boolean; mode: 'external' | 'vpn' | 'none'; token?: string; username?: string; password?: string; } // Get auth configuration from environment export function getAuthConfig(): AuthConfig { const mode = env.MORPHBOX_AUTH_MODE || 'none'; const enabled = mode === 'external' || (mode === 'vpn' && env.MORPHBOX_AUTH_ENABLED === 'true'); return { enabled, mode: mode as 'external' | 'vpn' | 'none', token: env.MORPHBOX_AUTH_TOKEN, username: env.MORPHBOX_AUTH_USERNAME, password: env.MORPHBOX_AUTH_PASSWORD }; } // Generate a secure random token export function generateAuthToken(): string { return crypto.randomBytes(32).toString('hex'); } // Validate authentication token export function validateToken(token: string | null): boolean { const config = getAuthConfig(); if (!config.enabled) { return true; // Auth disabled } if (!token) { return false; } // Check against stored token or generated session token const storedToken = config.token || env.MORPHBOX_AUTH_TOKEN; if (!storedToken) { return false; } // For cookies, we accept "authenticated" as a valid token for backward compatibility if (token === 'authenticated' && config.username && config.password) { return true; } try { return crypto.timingSafeEqual( Buffer.from(token), Buffer.from(storedToken) ); } catch { return false; } } // Validate basic auth credentials export function validateBasicAuth(username: string | null, password: string | null): boolean { const config = getAuthConfig(); if (!config.enabled) { return true; // Auth disabled } if (!username || !password || !config.username || !config.password) { return false; } const usernameMatch = crypto.timingSafeEqual( Buffer.from(username), Buffer.from(config.username) ); const passwordMatch = crypto.timingSafeEqual( Buffer.from(password), Buffer.from(config.password) ); return usernameMatch && passwordMatch; } // Extract auth credentials from request export function extractAuthCredentials(event: RequestEvent): { token?: string; username?: string; password?: string; } { const token = event.url.searchParams.get('token') || event.request.headers.get('x-auth-token') || event.cookies.get('morphbox-auth-token'); // Try to extract basic auth from Authorization header const authHeader = event.request.headers.get('authorization'); let username: string | undefined; let password: string | undefined; if (authHeader?.startsWith('Basic ')) { const base64Credentials = authHeader.slice(6); const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); const [user, pass] = credentials.split(':'); username = user; password = pass; } return { token, username, password }; } // Check if request is authenticated export function isAuthenticated(event: RequestEvent): boolean { const config = getAuthConfig(); if (!config.enabled) { return true; // Auth disabled } // SECURITY: Get client IP and check both IP allowlist and rate limiting const clientIp = event.getClientAddress(); // Check IP allowlist first if (!allowlistManager.isIPAllowed(clientIp)) { console.warn(`[Auth] Blocked connection from non-allowlisted IP: ${clientIp}`); return false; } // Then check rate limiting if (authRateLimiter.isBlocked(clientIp)) { console.warn(`[Auth] Blocked authentication attempt from ${clientIp} due to rate limiting`); return false; } const { token, username, password } = extractAuthCredentials(event); // Check token auth if (config.token && validateToken(token || null)) { authRateLimiter.recordSuccessfulAttempt(clientIp); return true; } // Check basic auth if (config.username && config.password && validateBasicAuth(username || '', password || '')) { authRateLimiter.recordSuccessfulAttempt(clientIp); return true; } // Record failed attempt authRateLimiter.recordFailedAttempt(clientIp); const remainingAttempts = authRateLimiter.getRemainingAttempts(clientIp); console.warn(`[Auth] Failed authentication from ${clientIp}. ${remainingAttempts} attempts remaining`); return false; } // WebSocket authentication export function validateWebSocketAuth(url: URL, headers: Record, clientIp?: string): boolean { const config = getAuthConfig(); if (!config.enabled) { return true; // Auth disabled } // SECURITY: Check rate limiting if IP is provided if (clientIp && authRateLimiter.isBlocked(clientIp)) { console.warn(`[Auth] Blocked WebSocket authentication from ${clientIp} due to rate limiting`); return false; } // Check token from query params or headers const token = url.searchParams.get('token') || headers['x-auth-token']; if (config.token && validateToken(token)) { if (clientIp) authRateLimiter.recordSuccessfulAttempt(clientIp); return true; } // Check basic auth from headers const authHeader = headers['authorization']; if (authHeader?.startsWith('Basic ')) { const base64Credentials = authHeader.slice(6); const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); const [username, password] = credentials.split(':'); if (validateBasicAuth(username, password)) { if (clientIp) authRateLimiter.recordSuccessfulAttempt(clientIp); return true; } } // Record failed attempt if (clientIp) { authRateLimiter.recordFailedAttempt(clientIp); const remainingAttempts = authRateLimiter.getRemainingAttempts(clientIp); console.warn(`[Auth] Failed WebSocket authentication from ${clientIp}. ${remainingAttempts} attempts remaining`); } return false; } // Login page HTML export function getLoginPageHTML(): string { return ` Morphbox - Authentication Required

🔐 Morphbox Authentication

⚠️ Security Notice: You are accessing Morphbox in external mode. Authentication is required to protect your system.
`; }