Ctrl K
ComunidadGuías…

server/api/McpOAuthRoute.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const express = require("express");
const rateLimit = require("express-rate-limit");
const { Op } = require("sequelize");
const {
  buildOAuthProtectedResourceMetadata,
  getOAuthProtectedResourceMetadataUrl,
} = require("@modelcontextprotocol/server");
const db = require("../models/models");
const verifyToken = require("../modules/verifyToken");
const verifyDataApiKey = require("../modules/verifyDataApiKey");
const { DATA_API_SCOPES } = require("../modules/dataApiAccess");
const oauth = require("../modules/mcp/oauth");

const handle = (action) => async (req, res) => {
  try {
    await action(req, res);
  } catch (error) {
    if (error.oauthCode === "invalid_client" && req.headers.authorization)
      res.set("WWW-Authenticate", 'Basic realm="Chartbrew"');
    res
      .status(error.oauthCode ? error.status : 500)
      .json({ error: error.oauthCode || "server_error" });
  }
};

module.exports = (app) => {
  const { issuer, resource, client } = oauth.urls();
  const metadata = {
    issuer,
    authorization_endpoint: `${issuer}/oauth/authorize`,
    token_endpoint: `${issuer}/oauth/token`,
    registration_endpoint: `${issuer}/oauth/register`,
    revocation_endpoint: `${issuer}/oauth/revoke`,
    scopes_supported: [...DATA_API_SCOPES],
    response_types_supported: ["code"],
    grant_types_supported: ["authorization_code", "refresh_token"],
    token_endpoint_auth_methods_supported: ["none", "client_secret_basic", "client_secret_post"],
    revocation_endpoint_auth_methods_supported: [
      "none",
      "client_secret_basic",
      "client_secret_post",
    ],
    code_challenge_methods_supported: ["S256"],
    authorization_response_iss_parameter_supported: true,
  };
  const resourceMetadata = buildOAuthProtectedResourceMetadata({
    oauthMetadata: metadata,
    resourceServerUrl: new URL(resource),
    scopesSupported: [...DATA_API_SCOPES],
    resourceName: "Chartbrew",
  });
  app.get("/.well-known/oauth-authorization-server", (req, res) => res.json(metadata));
  app.get(
    ["/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/mcp"],
    (req, res) => res.json(resourceMetadata)
  );
  app.use(
    "/oauth",
    (req, res, next) => {
      res.set({
        "Cache-Control": "no-store",
        "Pragma": "no-cache",
        "Referrer-Policy": "no-referrer",
      });
      if (Buffer.byteLength(JSON.stringify(req.body || {})) > 16384)
        return res.status(413).json({ error: "invalid_request" });
      return next();
    },
    express.urlencoded({ extended: false, limit: "16kb", parameterLimit: 30 }),
    rateLimit({ windowMs: 60000, limit: 60, legacyHeaders: false })
  );
  app.use("/oauth", (req, res, next) => {
    if (
      req.method === "POST" &&
      (!req.body || typeof req.body !== "object" || Array.isArray(req.body))
    )
      return res.status(400).json({ error: "invalid_request" });
    return next();
  });
  app.post(
    "/oauth/register",
    rateLimit({ windowMs: 3600000, limit: 20, legacyHeaders: false }),
    handle(async (req, res) => res.status(201).json(await oauth.register(req.body)))
  );
  app.get(
    "/oauth/authorize",
    handle(async (req, res) => res.redirect(await oauth.authorize(req.query)))
  );
  app.post(
    "/oauth/token",
    handle(async (req, res) => res.json(await oauth.exchange(req)))
  );
  app.post(
    "/oauth/revoke",
    handle(async (req, res) => {
      await oauth.revoke(req);
      res.status(200).end();
    })
  );

  const signedIn = [
    (req, res, next) => {
      if (
        !verifyDataApiKey.readBearerToken(req) ||
        (req.headers.origin && req.headers.origin !== new URL(client).origin)
      )
        return res.status(401).json({ error: "Sign in to Chartbrew to continue." });
      return next();
    },
    verifyToken.withErrorHandler(
      (req, res) => res.status(401).json({ error: "Sign in to Chartbrew to continue." }),
      {
        validateToken: (claims) => !claims.tokenType && !claims.aud && !claims.newEmail,
      }
    ),
  ];
  app.get(
    "/oauth/consent/:id",
    ...signedIn,
    handle(async (req, res) => res.json(await oauth.consentInfo(req.params.id, req.user.id)))
  );
  app.post(
    "/oauth/consent/:id",
    ...signedIn,
    handle(async (req, res) => res.json(await oauth.consent(req.params.id, req.user.id, req.body)))
  );
  app.get(
    "/oauth/apps",
    ...signedIn,
    handle(async (req, res) => {
      const apps = await db.McpOAuthGrant.findAll({
        where: {
          user_id: req.user.id,
          revokedAt: null,
          exchangedAt: { [Op.ne]: null },
          expiresAt: { [Op.gt]: new Date() },
        },
        include: [
          { model: db.McpOAuthClient, attributes: ["name"] },
          { model: db.Team, attributes: ["name"] },
        ],
        order: [["createdAt", "DESC"]],
      });
      res.json(
        apps.map((grant) => ({
          id: grant.id,
          name: grant.McpOAuthClient.name,
          team: grant.Team.name,
          scopes: grant.scopes,
          allProjects: grant.all_projects,
          createdAt: grant.createdAt,
          expiresAt: grant.expiresAt,
        }))
      );
    })
  );
  app.delete(
    "/oauth/apps/:id",
    ...signedIn,
    handle(async (req, res) => {
      await db.McpOAuthGrant.update(
        { revokedAt: new Date() },
        { where: { id: req.params.id, user_id: req.user.id } }
      );
      res.json({ success: true });
    })
  );
  return (req, res, next) => next();
};

module.exports.verifyMcpAuth = async (req, res, next) => {
  const { resource } = oauth.urls();
  res.set(
    "WWW-Authenticate",
    `Bearer resource_metadata="${getOAuthProtectedResourceMetadataUrl(new URL(resource))}", scope="${[...DATA_API_SCOPES].join(" ")}"`
  );
  res.set("Access-Control-Expose-Headers", "WWW-Authenticate, MCP-Protocol-Version");
  const token = verifyDataApiKey.readBearerToken(req);
  if (!token?.startsWith("cbm_"))
    return verifyDataApiKey()(req, res, () => {
      res.removeHeader("WWW-Authenticate");
      next();
    });
  try {
    req.apiKeyAccess = await oauth.verifyAccess(token);
    res.removeHeader("WWW-Authenticate");
    return next();
  } catch {
    res.set("WWW-Authenticate", `${res.get("WWW-Authenticate")}, error="invalid_token"`);
    return res.status(401).json({ error: "invalid_token" });
  }
};