diff --git a/.env.example b/.env.example index a7fcb54..1e5bf36 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,8 @@ TUNNEL_TOKEN= # CORS_ENABLED=true # CORS_ALLOW_ALL=false # CORS_ORIGINS=https://app1.com,https://app2.com + +# API Authentication - Protect your API endpoints +# AUTH_ENABLED=true # Enable authentication (required for production) +# API_KEYS=sk-key1,sk-key2,sk-key3 # Comma-separated API keys (ONLY via env var for security) +# AUTH_PUBLIC_MODELS=true # Allow /v1/models without auth diff --git a/auth-middleware.js b/auth-middleware.js new file mode 100644 index 0000000..3fdad8a --- /dev/null +++ b/auth-middleware.js @@ -0,0 +1,146 @@ +/** + * 请求认证中间件 + * 验证客户端请求的 API Key,保护 API 端点 + */ + +import { getConfig } from './config.js'; +import { logInfo, logError } from './logger.js'; + +// 不需要认证的路径(精确匹配) +const PUBLIC_PATHS = new Set([ + '/', + '/health', + '/status' +]); + +// 可配置是否公开的路径 +const OPTIONAL_PUBLIC_PATHS = new Set([ + '/v1/models' +]); + +/** + * 获取认证配置 + * API Keys 只从环境变量读取(安全考虑) + * enabled/public_models 可从 config.json 读取默认值,环境变量可覆盖 + */ +export function getAuthConfig() { + const cfg = getConfig(); + const configAuth = cfg.auth || {}; + + // 环境变量 + const envEnabled = process.env.AUTH_ENABLED; + const envApiKeys = process.env.API_KEYS; + const envPublicModels = process.env.AUTH_PUBLIC_MODELS; + + // 解析 enabled(环境变量 > config.json) + let enabled = configAuth.enabled ?? false; + if (envEnabled !== undefined) { + enabled = ['true', '1', 'yes'].includes(envEnabled.toLowerCase()); + } + + // API Keys 只从环境变量读取(敏感信息不应存储在配置文件中) + let apiKeys = []; + if (envApiKeys) { + apiKeys = envApiKeys.split(',').map(k => k.trim()).filter(k => k); + } + + // 解析 public_models(环境变量 > config.json) + let publicModels = configAuth.public_models ?? true; + if (envPublicModels !== undefined) { + publicModels = ['true', '1', 'yes'].includes(envPublicModels.toLowerCase()); + } + + return { + enabled, + apiKeys: new Set(apiKeys), + publicModels + }; +} + +/** + * 从请求头中提取 API Key + * 支持: Authorization: Bearer 或 x-api-key: + */ +function extractApiKey(req) { + // 优先检查 Authorization header + const authHeader = req.headers.authorization; + if (authHeader) { + if (authHeader.startsWith('Bearer ')) { + return authHeader.slice(7).trim(); + } + // 也支持直接传 key(不带 Bearer 前缀) + return authHeader.trim(); + } + + // 其次检查 x-api-key header + const xApiKey = req.headers['x-api-key']; + if (xApiKey) { + return xApiKey.trim(); + } + + return null; +} + +/** + * 认证中间件 + */ +export function authMiddleware(req, res, next) { + const authConfig = getAuthConfig(); + + // 如果认证未启用,直接放行 + if (!authConfig.enabled) { + return next(); + } + + // 检查是否是公开路径 + if (PUBLIC_PATHS.has(req.path)) { + return next(); + } + + // 检查可选公开路径 + if (authConfig.publicModels && OPTIONAL_PUBLIC_PATHS.has(req.path)) { + return next(); + } + + // 检查 API Keys 是否配置 + if (authConfig.apiKeys.size === 0) { + logError('Auth enabled but no API keys configured'); + return res.status(500).json({ + error: { + message: 'Server configuration error: authentication enabled but no API keys configured', + type: 'server_error', + code: 'auth_not_configured' + } + }); + } + + // 提取并验证 API Key + const clientKey = extractApiKey(req); + + if (!clientKey) { + logInfo(`Auth failed: No API key provided for ${req.method} ${req.path}`); + return res.status(401).json({ + error: { + message: 'Missing API key. Please include your API key in the Authorization header using Bearer auth (Authorization: Bearer YOUR_API_KEY) or as x-api-key header.', + type: 'authentication_error', + code: 'missing_api_key' + } + }); + } + + if (!authConfig.apiKeys.has(clientKey)) { + logInfo(`Auth failed: Invalid API key for ${req.method} ${req.path}`); + return res.status(401).json({ + error: { + message: 'Invalid API key provided.', + type: 'authentication_error', + code: 'invalid_api_key' + } + }); + } + + // 认证通过 + next(); +} + +export default authMiddleware; diff --git a/config.json b/config.json index e6f635e..57f96e9 100644 --- a/config.json +++ b/config.json @@ -100,6 +100,10 @@ "http://127.0.0.1:3000" ] }, + "auth": { + "enabled": false, + "public_models": true + }, "dev_mode": false, "user_agent": "factory-cli/0.40.2", "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n" diff --git a/server.js b/server.js index b974e2d..a516227 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ import { initializeAuth } from './auth.js'; import { initializeUserAgentUpdater } from './user-agent-updater.js'; import './sls-logger.js'; // 初始化阿里云日志服务 import { sanitizeForLog } from './log-sanitizer.js'; +import { authMiddleware, getAuthConfig } from './auth-middleware.js'; // ============================================================================ // 全局错误处理 - 必须在应用启动前注册 @@ -147,6 +148,9 @@ app.use((req, res, next) => { next(); }); +// 请求认证中间件 +app.use(authMiddleware); + app.use(router); app.get('/', (req, res) => { @@ -243,6 +247,14 @@ app.use((err, req, res, next) => { loadConfig(); logInfo('Configuration loaded successfully'); logInfo(`Dev mode: ${isDevMode()}`); + + // Log auth status + const authConfig = getAuthConfig(); + if (authConfig.enabled) { + logInfo(`Auth enabled with ${authConfig.apiKeys.size} API key(s)`); + } else { + logInfo('Auth disabled - API endpoints are publicly accessible'); + } // Initialize User-Agent version updater initializeUserAgentUpdater();