claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
368 lines (367 loc) • 14.1 kB
JavaScript
/**
* MCP Authentication Middleware
* Token-based authentication and authorization for MCP containers
*/ const crypto = require('crypto');
const Redis = require('redis');
const fs = require('fs').promises;
const path = require('path');
let MCPAuthMiddleware = class MCPAuthMiddleware {
constructor(options = {}){
this.redis = null;
this.options = {
// FIX: Default to 'localhost' for host execution, Docker deployments should set CFN_REDIS_HOST explicitly
redisUrl: options.redisUrl || process.env.CFN_REDIS_URL || process.env.MCP_REDIS_URL || `redis://${process.env.CFN_REDIS_HOST || 'localhost'}:${process.env.CFN_REDIS_PORT || 6379}`,
tokenExpiry: options.tokenExpiry || '24h',
authRequired: options.authRequired !== false,
rateLimitWindow: options.rateLimitWindow || 60,
rateLimitMax: options.rateLimitMax || 100,
agentConfigPath: options.agentConfigPath || './config/agent-whitelist.json',
skillConfigPath: options.skillConfigPath || './config/skill-requirements.json',
...options
};
this.agentWhitelist = new Map();
this.skillRequirements = new Map();
this.rateLimitCache = new Map();
}
/**
* Initialize authentication middleware
*/ async initialize() {
try {
// Connect to Redis
this.redis = Redis.createClient({
url: this.options.redisUrl
});
await this.redis.connect();
console.log('[MCPAuth] Connected to Redis for authentication');
// Load configuration
await this.loadAgentWhitelist();
await this.loadSkillRequirements();
// Start cleanup interval
this.startCleanupInterval();
console.log('[MCPAuth] Authentication middleware initialized successfully');
} catch (error) {
console.error('[MCPAuth] Failed to initialize:', error);
throw error;
}
}
/**
* Load agent whitelist from configuration
*/ async loadAgentWhitelist() {
try {
const configPath = path.resolve(this.options.agentConfigPath);
const config = await fs.readFile(configPath, 'utf8');
const whitelist = JSON.parse(config);
this.agentWhitelist.clear();
for (const agent of whitelist.agents){
this.agentWhitelist.set(agent.type, agent);
}
console.log(`[MCPAuth] Loaded ${this.agentWhitelist.size} agent configurations`);
} catch (error) {
console.warn('[MCPAuth] Failed to load agent whitelist:', error.message);
// Default to empty whitelist (deny all)
this.agentWhitelist.clear();
}
}
/**
* Load skill requirements from configuration
*/ async loadSkillRequirements() {
try {
const configPath = path.resolve(this.options.skillConfigPath);
const config = await fs.readFile(configPath, 'utf8');
const requirements = JSON.parse(config);
this.skillRequirements.clear();
for (const [tool, req] of Object.entries(requirements.tools)){
this.skillRequirements.set(tool, req);
}
console.log(`[MCPAuth] Loaded ${this.skillRequirements.size} tool skill requirements`);
} catch (error) {
console.warn('[MCPAuth] Failed to load skill requirements:', error.message);
this.skillRequirements.clear();
}
}
/**
* Authenticate agent request
*/ async authenticateRequest(request, response, next) {
try {
// Skip authentication if not required
if (!this.options.authRequired) {
return next();
}
// Extract authentication headers
const agentToken = request.headers['x-agent-token'];
const agentType = request.headers['x-agent-type'];
const toolName = request.body?.name || request.params?.toolName;
// Validate required headers
if (!agentToken || !agentType) {
return this.sendErrorResponse(response, 401, 'Missing authentication headers', {
required: [
'x-agent-token',
'x-agent-type'
]
});
}
// Validate token in Redis
const tokenData = await this.validateToken(agentToken, agentType);
if (!tokenData) {
return this.sendErrorResponse(response, 401, 'Invalid or expired token', {
agentType,
tokenValid: false
});
}
// Validate agent type against whitelist
if (!this.isAgentAuthorized(agentType)) {
return this.sendErrorResponse(response, 403, 'Agent type not authorized', {
agentType,
allowedTypes: Array.from(this.agentWhitelist.keys())
});
}
// Validate skill-based tool access
if (toolName && !this.authorizeToolAccess(agentType, toolName)) {
const toolRequirements = this.skillRequirements.get(toolName);
return this.sendErrorResponse(response, 403, 'Insufficient skills for tool access', {
agentType,
toolName,
requiredSkills: toolRequirements?.requiredSkills || [],
agentSkills: tokenData.skills || []
});
}
// Check rate limits
if (!this.checkRateLimit(agentType, agentToken)) {
return this.sendErrorResponse(response, 429, 'Rate limit exceeded', {
agentType,
window: this.options.rateLimitWindow,
maxRequests: this.options.rateLimitMax
});
}
// Add agent context to request
request.agentContext = {
agentType,
agentToken,
skills: tokenData.skills || [],
authenticated: true,
timestamp: Date.now()
};
// Update last activity
await this.updateAgentActivity(agentToken, agentType);
return next();
} catch (error) {
console.error('[MCPAuth] Authentication error:', error);
return this.sendErrorResponse(response, 500, 'Authentication error', {
error: error.message
});
}
}
/**
* Validate token against Redis
*/ async validateToken(token, agentType) {
try {
const key = `mcp:agent:${agentType}:${token}`;
const tokenData = await this.redis.get(key);
if (!tokenData) {
return null;
}
const data = JSON.parse(tokenData);
// Check expiration
if (Date.now() > data.expiresAt) {
await this.redis.del(key);
return null;
}
return data;
} catch (error) {
console.error('[MCPAuth] Token validation error:', error);
return null;
}
}
/**
* Check if agent type is authorized
*/ isAgentAuthorized(agentType) {
return this.agentWhitelist.has(agentType);
}
/**
* Authorize tool access based on skills
*/ authorizeToolAccess(agentType, toolName) {
const toolRequirements = this.skillRequirements.get(toolName);
if (!toolRequirements) {
// No skill requirements defined - allow access
return true;
}
const agentConfig = this.agentWhitelist.get(agentType);
if (!agentConfig) {
return false;
}
const requiredSkills = toolRequirements.requiredSkills || [];
const agentSkills = agentConfig.skills || [];
// Check if agent has all required skills
return requiredSkills.every((skill)=>agentSkills.includes(skill));
}
/**
* Check rate limits for agent
*/ checkRateLimit(agentType, token) {
const now = Date.now();
const windowStart = now - this.options.rateLimitWindow * 1000;
const key = `${agentType}:${token}`;
if (!this.rateLimitCache.has(key)) {
this.rateLimitCache.set(key, []);
}
const requests = this.rateLimitCache.get(key);
// Remove old requests outside window
const validRequests = requests.filter((timestamp)=>timestamp > windowStart);
this.rateLimitCache.set(key, validRequests);
// Check if under limit
if (validRequests.length >= this.options.rateLimitMax) {
return false;
}
// Add current request
validRequests.push(now);
return true;
}
/**
* Update agent activity in Redis
*/ async updateAgentActivity(token, agentType) {
try {
const key = `mcp:agent:${agentType}:${token}`;
const activity = {
lastActivity: Date.now(),
requestCount: 1
};
await this.redis.hIncrBy(key, 'requestCount', 1);
await this.redis.hSet(key, 'lastActivity', Date.now().toString());
await this.redis.expire(key, this.parseExpiry(this.options.tokenExpiry));
} catch (error) {
console.error('[MCPAuth] Failed to update activity:', error);
}
}
/**
* Generate agent token
*/ generateAgentToken(agentType, skills = [], expiresIn = null) {
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = Date.now() + this.parseExpiry(expiresIn || this.options.tokenExpiry) * 1000;
return {
token,
agentType,
skills,
expiresAt,
createdAt: Date.now()
};
}
/**
* Register agent token in Redis
*/ async registerAgentToken(tokenData) {
try {
const key = `mcp:agent:${tokenData.agentType}:${tokenData.token}`;
const value = JSON.stringify(tokenData);
await this.redis.setEx(key, this.parseExpiry(this.options.tokenExpiry), value);
console.log(`[MCPAuth] Registered token for agent: ${tokenData.agentType}`);
return tokenData;
} catch (error) {
console.error('[MCPAuth] Failed to register token:', error);
throw error;
}
}
/**
* Revoke agent token
*/ async revokeAgentToken(agentType, token) {
try {
const key = `mcp:agent:${agentType}:${token}`;
await this.redis.del(key);
console.log(`[MCPAuth] Revoked token for agent: ${agentType}`);
return true;
} catch (error) {
console.error('[MCPAuth] Failed to revoke token:', error);
return false;
}
}
/**
* Send error response
*/ sendErrorResponse(response, statusCode, message, details = null) {
const errorResponse = {
jsonrpc: '2.0',
error: {
code: statusCode === 401 ? -32001 : statusCode === 403 ? -32002 : -32003,
message,
...details && {
details
}
},
id: response.body?.id || null
};
response.writeHead(statusCode, {
'Content-Type': 'application/json'
});
response.end(JSON.stringify(errorResponse));
}
/**
* Parse expiry string to seconds
*/ parseExpiry(expiry) {
if (typeof expiry === 'number') {
return expiry;
}
const match = expiry.match(/^(\d+)([smhd])$/);
if (!match) {
return 3600; // Default to 1 hour
}
const value = parseInt(match[1]);
const unit = match[2];
const multipliers = {
s: 1,
m: 60,
h: 3600,
d: 86400
};
return value * (multipliers[unit] || 3600);
}
/**
* Start cleanup interval for rate limit cache
*/ startCleanupInterval() {
setInterval(()=>{
const now = Date.now();
const windowStart = now - this.options.rateLimitWindow * 1000;
for (const [key, requests] of this.rateLimitCache.entries()){
const validRequests = requests.filter((timestamp)=>timestamp > windowStart);
if (validRequests.length === 0) {
this.rateLimitCache.delete(key);
} else {
this.rateLimitCache.set(key, validRequests);
}
}
}, 60000); // Clean up every minute
}
/**
* Get authentication statistics
*/ async getStats() {
try {
const stats = {
registeredAgents: this.agentWhitelist.size,
configuredTools: this.skillRequirements.size,
rateLimitedClients: this.rateLimitCache.size,
redisConnected: this.redis?.isOpen || false
};
// Get active tokens count
if (this.redis?.isOpen) {
const keys = await this.redis.keys('mcp:agent:*');
stats.activeTokens = keys.length;
}
return stats;
} catch (error) {
console.error('[MCPAuth] Failed to get stats:', error);
return {
error: error.message
};
}
}
/**
* Shutdown authentication middleware
*/ async shutdown() {
try {
if (this.redis) {
await this.redis.quit();
this.redis = null;
}
console.log('[MCPAuth] Authentication middleware shutdown complete');
} catch (error) {
console.error('[MCPAuth] Shutdown error:', error);
}
}
};
module.exports = MCPAuthMiddleware;
//# sourceMappingURL=auth-middleware.js.map