UNPKG

backend-mcp

Version:

Generador automático de backends con Node.js, Express, Prisma y módulos configurables. Servidor MCP compatible con npx para agentes IA. Soporta PostgreSQL, MySQL, MongoDB y SQLite.

371 lines (322 loc) 10.3 kB
/** * Módulo de autenticación JWT para Backend MCP * Protege el acceso a las herramientas mediante tokens JWT */ const https = require('https'); const crypto = require('crypto'); const authConfig = require('./auth-config'); // Cache de tokens válidos (en memoria) const tokenCache = new Map(); // Configuración exportada para compatibilidad const AUTH_CONFIG = { API_URL: authConfig.api.baseUrl + authConfig.api.endpoints.validate, JWT_SECRET: authConfig.jwt.secret, tokenCache, CACHE_TTL: authConfig.cache.ttl, DEMO_TOKENS: authConfig.development.demoTokens }; /** * Valida un token JWT * @param {string} token - El token JWT a validar * @returns {Promise<Object>} - Información del usuario si es válido */ async function validateJWT(token) { if (!token) { throw new Error('Token JWT requerido'); } // Verificar cache primero const cached = tokenCache.get(token); if (cached && Date.now() < cached.expiry) { return cached.user; } let user; try { // Siempre intentar validación local primero (incluye tokens de demo) try { user = await validateLocal(token); } catch (error) { console.warn('⚠️ Validación local falló, intentando validación remota:', error.message); user = await validateRemote(token); } // Guardar en cache tokenCache.set(token, { user, expiry: Date.now() + (authConfig.cache.ttl) }); return user; } catch (error) { // Limpiar del cache si existe tokenCache.delete(token); throw error; } } /** * Validación remota del token * @param {string} token - Token a validar * @returns {Promise<Object>} - Información del usuario */ function validateRemote(token) { return new Promise((resolve, reject) => { const url = new URL(authConfig.api.baseUrl + authConfig.api.endpoints.validate); const postData = JSON.stringify({ token }); const options = { hostname: url.hostname, port: url.port || 443, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), 'User-Agent': 'Backend-MCP/2.0.0' }, timeout: 5000 // 5 segundos timeout }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const response = JSON.parse(data); if (res.statusCode === 200 && response.valid) { resolve({ id: response.user.id, email: response.user.email, plan: response.user.plan || 'basic', permissions: response.user.permissions || ['basic'], validUntil: response.user.validUntil }); } else { reject(new Error(response.error || 'Token inválido')); } } catch (error) { reject(new Error('Respuesta inválida del servidor de autenticación')); } }); }); req.on('error', (error) => { reject(new Error(`Error de conexión: ${error.message}`)); }); req.on('timeout', () => { req.destroy(); reject(new Error('Timeout en validación remota')); }); req.write(postData); req.end(); }); } /** * Validación local del token (fallback) * @param {string} token - Token a validar * @returns {Promise<Object>} - Información del usuario */ async function validateLocal(token) { try { // Verificar si es un token de demostración const demoTokens = authConfig.development.demoTokens; if (Object.values(demoTokens).includes(token)) { let plan, permissions; if (token === demoTokens.basic) { plan = 'basic'; permissions = ['basic']; } else if (token === demoTokens.premium) { plan = 'premium'; permissions = ['basic', 'premium']; } else if (token === demoTokens.enterprise) { plan = 'enterprise'; permissions = ['basic', 'premium', 'enterprise']; } return { id: 'demo-user', email: `demo-${plan}@backend-mcp.com`, plan: plan, permissions: permissions, validUntil: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30 días }; } // Decodificar JWT básico (sin librerías externas) const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Formato de token inválido'); } const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString()); const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()); const signature = parts[2]; // Verificar expiración if (payload.exp && Date.now() >= payload.exp * 1000) { throw new Error('Token expirado'); } // Verificar firma (básico) const expectedSignature = crypto .createHmac('sha256', authConfig.jwt.secret) .update(`${parts[0]}.${parts[1]}`) .digest('base64url'); if (signature !== expectedSignature) { throw new Error('Firma de token inválida'); } // Verificar issuer if (payload.iss !== 'backend-mcp-auth') { throw new Error('Issuer de token inválido'); } return { id: payload.sub, email: payload.email, plan: payload.plan || 'basic', permissions: payload.permissions || ['basic'], validUntil: payload.exp }; } catch (error) { throw new Error(`Validación local falló: ${error.message}`); } } /** * Verifica si el usuario tiene permisos para usar una herramienta específica * @param {Object} userData - Datos del usuario del token * @param {string} toolName - Nombre de la herramienta * @returns {boolean} - true si tiene permisos */ function hasToolPermission(userData, toolName) { if (!userData || !userData.permissions) { return false; } const requiredPlan = authConfig.getRequiredPlan(toolName); return userData.permissions.includes(requiredPlan); } /** * Extrae el token JWT de diferentes fuentes * @param {Object} args - Argumentos del proceso o parámetros * @returns {string|null} - Token JWT o null si no se encuentra */ function extractToken(args = {}) { // 1. Desde argumentos de línea de comandos (contexto o process.argv) let argv = args.args || process.argv; const jwtIndex = argv.findIndex(arg => arg === '--jwt' || arg === '--token'); if (jwtIndex !== -1 && argv[jwtIndex + 1]) { return argv[jwtIndex + 1]; } // 2. Desde variable de entorno if (process.env.BACKEND_MCP_JWT) { return process.env.BACKEND_MCP_JWT; } // 3. Desde parámetros del mensaje MCP if (args.jwt || args.token) { return args.jwt || args.token; } // 4. Desde estructura de mensaje MCP if (args.mcpMessage && args.mcpMessage.params && args.mcpMessage.params.arguments) { const mcpArgs = args.mcpMessage.params.arguments; if (mcpArgs.jwt || mcpArgs.token) { return mcpArgs.jwt || mcpArgs.token; } } // 5. Desde headers (si están disponibles) if (args.headers && args.headers.authorization) { const auth = args.headers.authorization; if (auth.startsWith('Bearer ')) { return auth.substring(7); } } return null; } /** * Middleware de autenticación para llamadas a herramientas * @param {string} toolName - Nombre de la herramienta * @param {Object} params - Parámetros de la herramienta * @param {Object} context - Contexto de la llamada (argumentos, variables de entorno, etc.) * @returns {Promise<Object>} - Resultado de la autenticación */ async function authenticateToolCall(toolName, params, context = {}) { try { // Bypass en desarrollo si está configurado if (authConfig.development.bypassAuth) { if (authConfig.development.verboseLogging) { console.log('🔓 Bypass de autenticación activado (desarrollo)'); } return { success: true, user: { plan: 'enterprise', permissions: ['basic', 'premium', 'enterprise'] }, token: 'development-bypass' }; } // Extraer token de diferentes fuentes const token = extractToken(context); if (!token) { return { success: false, error: authConfig.getMessage('noToken', 'es'), code: 'AUTH_REQUIRED' }; } // Validar token const user = await validateJWT(token); // Verificar permisos para la herramienta if (!hasToolPermission(user, toolName)) { const requiredPlan = authConfig.getRequiredPlan(toolName); return { success: false, error: authConfig.getMessage('insufficientPermissions', 'es'), code: 'INSUFFICIENT_PERMISSIONS', requiredPlan: requiredPlan, currentPlan: user.plan, toolName: toolName }; } return { success: true, user: user, token: token }; } catch (error) { if (authConfig.development.verboseLogging) { console.error('Error en autenticación:', error); } // Determinar el tipo de error const isNetworkError = error.message.includes('Error de conexión') || error.message.includes('Timeout') || error.message.includes('Network error'); if (isNetworkError) { return { success: false, error: authConfig.getMessage('networkError', 'es'), code: 'NETWORK_ERROR', details: error.message }; } else { return { success: false, error: authConfig.getMessage('invalidToken', 'es'), code: 'INVALID_TOKEN', details: error.message }; } } } /** * Limpia el cache de tokens (útil para testing) */ function clearTokenCache() { tokenCache.clear(); } /** * Obtiene estadísticas del cache */ function getCacheStats() { return { size: tokenCache.size, entries: Array.from(tokenCache.entries()).map(([token, data]) => ({ token: token.substring(0, 10) + '...', expiry: new Date(data.expiry).toISOString(), user: data.user.email })) }; } module.exports = { validateJWT, hasToolPermission, extractToken, authenticateToolCall, clearTokenCache, getCacheStats, AUTH_CONFIG };