mcp remoto token
This commit is contained in:
95
mcp-server/auth/mcpTokens.js
Normal file
95
mcp-server/auth/mcpTokens.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* MCP Token Validation
|
||||
*
|
||||
* Valida tokens X-MCP-Secret contra Redis.
|
||||
* El backend Python escribe la clave `mcp_tokens:<sha256_hex>` con metadata JSON:
|
||||
* {
|
||||
* "id": "...",
|
||||
* "user": "superadmin",
|
||||
* "project": "vegaasesores.com",
|
||||
* "label": "MacBook Pro",
|
||||
* "createdAt": 1234567890,
|
||||
* "lastUsedAt": null | 1234567900
|
||||
* }
|
||||
*
|
||||
* El plaintext es "acai_<43 chars>" y se hashea con sha256 hex en minusculas.
|
||||
*/
|
||||
|
||||
import Redis from "ioredis";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const REDIS_URL = process.env.REDIS_URL || "redis://redis:6379";
|
||||
|
||||
// Cliente Redis compartido (lazy init para no conectar si el MCP corre en modo stdio
|
||||
// u otros escenarios donde el middleware HTTP nunca se invoque).
|
||||
let redisClient = null;
|
||||
|
||||
function getRedis() {
|
||||
if (redisClient) return redisClient;
|
||||
try {
|
||||
redisClient = new Redis(REDIS_URL, {
|
||||
lazyConnect: false,
|
||||
maxRetriesPerRequest: 3,
|
||||
enableReadyCheck: false,
|
||||
});
|
||||
redisClient.on("error", (err) => {
|
||||
console.error("[mcp-tokens] redis error:", err.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[mcp-tokens] no se pudo inicializar redis:", e.message);
|
||||
redisClient = null;
|
||||
}
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashea un string con SHA256 y devuelve hex en minusculas.
|
||||
*/
|
||||
export function sha256Hex(str) {
|
||||
return crypto.createHash("sha256").update(str, "utf8").digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida un X-MCP-Secret plaintext contra Redis.
|
||||
* @param {string} secret - plaintext tipo "acai_xxx"
|
||||
* @returns {Promise<{user: string, project: string, id: string} | null>}
|
||||
*/
|
||||
export async function validateMcpToken(secret) {
|
||||
if (!secret || typeof secret !== "string") return null;
|
||||
const r = getRedis();
|
||||
if (!r) return null;
|
||||
|
||||
const sha = sha256Hex(secret);
|
||||
const key = `mcp_tokens:${sha}`;
|
||||
|
||||
let raw;
|
||||
try {
|
||||
raw = await r.get(key);
|
||||
} catch (err) {
|
||||
console.error("[mcp-tokens] redis GET error:", err.message);
|
||||
return null;
|
||||
}
|
||||
if (!raw) return null;
|
||||
|
||||
let meta;
|
||||
try {
|
||||
meta = JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!meta || !meta.user || !meta.project) return null;
|
||||
|
||||
// Actualizacion asincrona de lastUsedAt — no bloqueamos la request.
|
||||
updateLastUsedAt(key, meta).catch((e) => {
|
||||
console.error("[mcp-tokens] lastUsedAt update failed:", e.message);
|
||||
});
|
||||
|
||||
return { user: meta.user, project: meta.project, id: meta.id || "" };
|
||||
}
|
||||
|
||||
async function updateLastUsedAt(key, meta) {
|
||||
const r = getRedis();
|
||||
if (!r) return;
|
||||
const next = { ...meta, lastUsedAt: Math.floor(Date.now() / 1000) };
|
||||
await r.set(key, JSON.stringify(next));
|
||||
}
|
||||
Reference in New Issue
Block a user