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
JavaScript
/**
* 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
};