import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { timingSafeEqual } from "crypto"; /** * ApiKeyGuard - Authentication guard for service-to-service communication * * Validates the X-API-Key header against the COORDINATOR_API_KEY environment variable. * Uses constant-time comparison to prevent timing attacks. * * Usage: * @UseGuards(ApiKeyGuard) * @Controller('coordinator') * export class CoordinatorIntegrationController { ... } */ @Injectable() export class ApiKeyGuard implements CanActivate { constructor(private readonly configService: ConfigService) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest<{ headers: Record }>(); const providedKey = this.extractApiKeyFromHeader(request); if (!providedKey) { throw new UnauthorizedException("No API key provided"); } const configuredKey = this.configService.get("COORDINATOR_API_KEY"); if (!configuredKey) { throw new UnauthorizedException("API key authentication not configured"); } if (!this.isValidApiKey(providedKey, configuredKey)) { throw new UnauthorizedException("Invalid API key"); } return true; } /** * Extract API key from X-API-Key header (case-insensitive) */ private extractApiKeyFromHeader(request: { headers: Record; }): string | undefined { const headers = request.headers; // Check common variations (lowercase, uppercase, mixed case) const apiKey = headers["x-api-key"] ?? headers["X-API-Key"] ?? headers["X-Api-Key"] ?? headers["x-api-key"]; // Return undefined if key is empty string if (typeof apiKey === "string" && apiKey.trim() === "") { return undefined; } return apiKey; } /** * Validate API key using constant-time comparison to prevent timing attacks */ private isValidApiKey(providedKey: string, configuredKey: string): boolean { try { // Convert strings to buffers for constant-time comparison const providedBuffer = Buffer.from(providedKey, "utf8"); const configuredBuffer = Buffer.from(configuredKey, "utf8"); // Keys must be same length for timingSafeEqual if (providedBuffer.length !== configuredBuffer.length) { return false; } return timingSafeEqual(providedBuffer, configuredBuffer); } catch { // If comparison fails for any reason, reject return false; } } }