UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

109 lines (108 loc) 4.33 kB
"use strict"; /** * Dual-mode authentication middleware for PRD #380. * * Validates Bearer tokens in two modes: * 1. JWT (HMAC-SHA256) — returns UserIdentity when valid * 2. Legacy DOT_AI_AUTH_TOKEN — constant-time comparison fallback * * All existing tests continue to pass because the legacy path is preserved. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.checkBearerAuth = checkBearerAuth; exports.isAuthEnabled = isAuthEnabled; const node_crypto_1 = require("node:crypto"); const jwt_1 = require("./jwt"); /** * Check Bearer token authentication (dual-mode: JWT + legacy token). * * Order: JWT verification first, legacy DOT_AI_AUTH_TOKEN fallback second. * When neither auth method is configured, authentication is disabled (backward compatible). * * @param req - The incoming HTTP request * @returns AuthResult with optional UserIdentity when JWT auth succeeds */ function checkBearerAuth(req) { const legacyToken = process.env.DOT_AI_AUTH_TOKEN; const jwtSecret = process.env.DOT_AI_JWT_SECRET; // If no auth is configured, reject — DOT_AI_AUTH_TOKEN is required if (!legacyToken && !jwtSecret) { return { authorized: false, message: 'Authentication is not configured. Set DOT_AI_AUTH_TOKEN in your deployment.', }; } // Extract Authorization header, with X-Dot-AI-Authorization as fallback. // The fallback supports Kubernetes API server proxy scenarios where the // standard Authorization header is overwritten with a K8s bearer token. const rawAuthHeader = req.headers['x-dot-ai-authorization'] || req.headers['authorization']; if (!rawAuthHeader) { return { authorized: false, message: 'Authentication required. Provide Authorization: Bearer <token> header.', }; } // Normalize header to string (handle array case) const authHeader = Array.isArray(rawAuthHeader) ? (rawAuthHeader[0] ?? '') : rawAuthHeader; // Parse Bearer token (ReDoS-safe: indexOf instead of regex) const trimmedHeader = authHeader.trim(); const spaceIndex = trimmedHeader.indexOf(' '); if (spaceIndex === -1) { return { authorized: false, message: 'Invalid authorization format. Expected: Bearer <token>', }; } const scheme = trimmedHeader.slice(0, spaceIndex); const providedToken = trimmedHeader.slice(spaceIndex + 1).trim(); // Validate Bearer scheme (case-insensitive per RFC 7235) if (scheme.toLowerCase() !== 'bearer') { return { authorized: false, message: 'Invalid authorization format. Expected: Bearer <token>', }; } if (!providedToken) { return { authorized: false, message: 'Bearer token is empty.' }; } // Mode 1: Try JWT verification const secret = jwtSecret || (0, jwt_1.getJwtSecret)(); const claims = (0, jwt_1.verifyJwt)(providedToken, secret); if (claims) { return { authorized: true, identity: { userId: claims.sub, email: claims.email, groups: claims.groups ?? [], source: 'oauth', }, }; } // Mode 2: Fall back to legacy DOT_AI_AUTH_TOKEN comparison if (legacyToken) { const configuredBuffer = Buffer.from(legacyToken, 'utf8'); const providedBuffer = Buffer.from(providedToken, 'utf8'); if (configuredBuffer.length !== providedBuffer.length) { // Dummy comparison to maintain constant time (0, node_crypto_1.timingSafeEqual)(configuredBuffer, configuredBuffer); return { authorized: false, message: 'Invalid authentication token.' }; } if ((0, node_crypto_1.timingSafeEqual)(configuredBuffer, providedBuffer)) { return { authorized: true, identity: { userId: 'anonymous', groups: [], source: 'token' }, }; } } return { authorized: false, message: 'Invalid authentication token.' }; } /** * Check if authentication is enabled. * True when either legacy token or JWT secret is configured. */ function isAuthEnabled() { return !!process.env.DOT_AI_AUTH_TOKEN || !!process.env.DOT_AI_JWT_SECRET; }