import { timingSafeEqual } from 'node:crypto' import type { HttpContext } from '@adonisjs/core/http' import type { NextFn } from '@adonisjs/core/types/http' import env from '#start/env' /** * BasicAuthMiddleware * * Protects routes with HTTP Basic Authentication. * Credentials are configured via environment variables: * WEBUI_USERNAME * WEBUI_PASSWORD * * Security: * - Constant-time comparison (timingSafeEqual) to prevent timing attacks * * Responds with 401 + WWW-Authenticate header if credentials are missing or invalid. * * NOTE: This middleware is registered for potential future use (e.g. machine-to-machine * access to web-ui pages). Web-UI currently uses session-based auth (authSession). */ export default class BasicAuthMiddleware { async handle({ request, response }: HttpContext, next: NextFn) { const authHeader = request.header('authorization') || '' if (!authHeader.startsWith('Basic ')) { return this.#unauthorized(response) } const base64Credentials = authHeader.slice(6) let decoded: string try { decoded = Buffer.from(base64Credentials, 'base64').toString('utf-8') } catch { return this.#unauthorized(response) } // Split only on the first colon — password may contain colons const colonIndex = decoded.indexOf(':') if (colonIndex === -1) { return this.#unauthorized(response) } const username = decoded.slice(0, colonIndex) const password = decoded.slice(colonIndex + 1) const expectedUsername = env.get('WEBUI_USERNAME')! const expectedPassword = env.get('WEBUI_PASSWORD')! const usernameMatch = safeCompare(username, expectedUsername) const passwordMatch = safeCompare(password, expectedPassword) if (!usernameMatch || !passwordMatch) { return this.#unauthorized(response) } return next() } #unauthorized(response: HttpContext['response']) { response.header('WWW-Authenticate', 'Basic realm="ADAM ACS"') return response.status(401).send('Unauthorized') } } /** * Constant-time string comparison to prevent timing side-channel attacks. */ function safeCompare(a: string, b: string): boolean { if (a.length !== b.length) return false return timingSafeEqual(Buffer.from(a), Buffer.from(b)) }