286 lines
6.7 KiB
JavaScript
286 lines
6.7 KiB
JavaScript
import jwt from "jsonwebtoken";
|
|
import { logger } from "../../scripts/modules/utils.js";
|
|
import crypto from "crypto";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
// Constants
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const API_KEYS_FILE =
|
|
process.env.MCP_API_KEYS_FILE || path.join(__dirname, "../api-keys.json");
|
|
const JWT_SECRET =
|
|
process.env.MCP_JWT_SECRET || "task-master-mcp-server-secret";
|
|
const JWT_EXPIRATION = process.env.MCP_JWT_EXPIRATION || "24h";
|
|
|
|
/**
|
|
* Authentication middleware and utilities for MCP server
|
|
*/
|
|
class MCPAuth {
|
|
constructor() {
|
|
this.apiKeys = new Map();
|
|
this.logger = logger;
|
|
this.loadApiKeys();
|
|
}
|
|
|
|
/**
|
|
* Load API keys from disk
|
|
*/
|
|
async loadApiKeys() {
|
|
try {
|
|
// Create API keys file if it doesn't exist
|
|
try {
|
|
await fs.access(API_KEYS_FILE);
|
|
} catch (error) {
|
|
// File doesn't exist, create it with a default admin key
|
|
const defaultApiKey = this.generateApiKey();
|
|
const defaultApiKeys = {
|
|
keys: [
|
|
{
|
|
id: "admin",
|
|
key: defaultApiKey,
|
|
role: "admin",
|
|
created: new Date().toISOString(),
|
|
},
|
|
],
|
|
};
|
|
|
|
await fs.mkdir(path.dirname(API_KEYS_FILE), { recursive: true });
|
|
await fs.writeFile(
|
|
API_KEYS_FILE,
|
|
JSON.stringify(defaultApiKeys, null, 2),
|
|
"utf8"
|
|
);
|
|
|
|
this.logger.info(
|
|
`Created default API keys file with admin key: ${defaultApiKey}`
|
|
);
|
|
}
|
|
|
|
// Load API keys
|
|
const data = await fs.readFile(API_KEYS_FILE, "utf8");
|
|
const apiKeys = JSON.parse(data);
|
|
|
|
apiKeys.keys.forEach((key) => {
|
|
this.apiKeys.set(key.key, {
|
|
id: key.id,
|
|
role: key.role,
|
|
created: key.created,
|
|
});
|
|
});
|
|
|
|
this.logger.info(`Loaded ${this.apiKeys.size} API keys`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to load API keys: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save API keys to disk
|
|
*/
|
|
async saveApiKeys() {
|
|
try {
|
|
const keys = [];
|
|
|
|
this.apiKeys.forEach((value, key) => {
|
|
keys.push({
|
|
id: value.id,
|
|
key,
|
|
role: value.role,
|
|
created: value.created,
|
|
});
|
|
});
|
|
|
|
await fs.writeFile(
|
|
API_KEYS_FILE,
|
|
JSON.stringify({ keys }, null, 2),
|
|
"utf8"
|
|
);
|
|
|
|
this.logger.info(`Saved ${keys.length} API keys`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to save API keys: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a new API key
|
|
* @returns {string} The generated API key
|
|
*/
|
|
generateApiKey() {
|
|
return crypto.randomBytes(32).toString("hex");
|
|
}
|
|
|
|
/**
|
|
* Create a new API key
|
|
* @param {string} id - Client identifier
|
|
* @param {string} role - Client role (admin, user)
|
|
* @returns {string} The generated API key
|
|
*/
|
|
async createApiKey(id, role = "user") {
|
|
const apiKey = this.generateApiKey();
|
|
|
|
this.apiKeys.set(apiKey, {
|
|
id,
|
|
role,
|
|
created: new Date().toISOString(),
|
|
});
|
|
|
|
await this.saveApiKeys();
|
|
|
|
this.logger.info(`Created new API key for ${id} with role ${role}`);
|
|
return apiKey;
|
|
}
|
|
|
|
/**
|
|
* Revoke an API key
|
|
* @param {string} apiKey - The API key to revoke
|
|
* @returns {boolean} True if the key was revoked
|
|
*/
|
|
async revokeApiKey(apiKey) {
|
|
if (!this.apiKeys.has(apiKey)) {
|
|
return false;
|
|
}
|
|
|
|
this.apiKeys.delete(apiKey);
|
|
await this.saveApiKeys();
|
|
|
|
this.logger.info(`Revoked API key`);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate an API key
|
|
* @param {string} apiKey - The API key to validate
|
|
* @returns {object|null} The API key details if valid, null otherwise
|
|
*/
|
|
validateApiKey(apiKey) {
|
|
return this.apiKeys.get(apiKey) || null;
|
|
}
|
|
|
|
/**
|
|
* Generate a JWT token for a client
|
|
* @param {string} clientId - Client identifier
|
|
* @param {string} role - Client role
|
|
* @returns {string} The JWT token
|
|
*/
|
|
generateToken(clientId, role) {
|
|
return jwt.sign({ clientId, role }, JWT_SECRET, {
|
|
expiresIn: JWT_EXPIRATION,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Verify a JWT token
|
|
* @param {string} token - The JWT token to verify
|
|
* @returns {object|null} The token payload if valid, null otherwise
|
|
*/
|
|
verifyToken(token) {
|
|
try {
|
|
return jwt.verify(token, JWT_SECRET);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to verify token: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Express middleware for API key authentication
|
|
* @param {object} req - Express request object
|
|
* @param {object} res - Express response object
|
|
* @param {function} next - Express next function
|
|
*/
|
|
authenticateApiKey(req, res, next) {
|
|
const apiKey = req.headers["x-api-key"];
|
|
|
|
if (!apiKey) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: "API key is required",
|
|
});
|
|
}
|
|
|
|
const keyDetails = this.validateApiKey(apiKey);
|
|
|
|
if (!keyDetails) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: "Invalid API key",
|
|
});
|
|
}
|
|
|
|
// Attach client info to request
|
|
req.client = {
|
|
id: keyDetails.id,
|
|
role: keyDetails.role,
|
|
};
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Express middleware for JWT authentication
|
|
* @param {object} req - Express request object
|
|
* @param {object} res - Express response object
|
|
* @param {function} next - Express next function
|
|
*/
|
|
authenticateToken(req, res, next) {
|
|
const authHeader = req.headers["authorization"];
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: "Authentication token is required",
|
|
});
|
|
}
|
|
|
|
const payload = this.verifyToken(token);
|
|
|
|
if (!payload) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: "Invalid or expired token",
|
|
});
|
|
}
|
|
|
|
// Attach client info to request
|
|
req.client = {
|
|
id: payload.clientId,
|
|
role: payload.role,
|
|
};
|
|
|
|
next();
|
|
}
|
|
|
|
/**
|
|
* Express middleware for role-based authorization
|
|
* @param {Array} roles - Array of allowed roles
|
|
* @returns {function} Express middleware
|
|
*/
|
|
authorizeRoles(roles) {
|
|
return (req, res, next) => {
|
|
if (!req.client || !req.client.role) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: "Unauthorized: Authentication required",
|
|
});
|
|
}
|
|
|
|
if (!roles.includes(req.client.role)) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: "Forbidden: Insufficient permissions",
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
}
|
|
|
|
export default MCPAuth;
|