branch:
auth-handler.ts
7164 bytesRaw
import type {
  AuthRequest,
  OAuthHelpers
} from "@cloudflare/workers-oauth-provider";
import { Hono } from "hono";

interface Env {
  OAUTH_PROVIDER: OAuthHelpers;
}

const app = new Hono<{ Bindings: Env }>();

/**
 * GET /authorize - OAuth authorization endpoint
 *
 * This endpoint is called when an MCP client wants to authorize.
 * In a full implementation, this would:
 * 1. Parse the OAuth request
 * 2. Check if the client is already approved (via cookie)
 * 3. Show an approval dialog or redirect to external OAuth provider
 */
app.get("/authorize", async (c) => {
  const oauthReqInfo: AuthRequest = await c.env.OAUTH_PROVIDER.parseAuthRequest(
    c.req.raw
  );
  const clientInfo = await c.env.OAUTH_PROVIDER.lookupClient(
    oauthReqInfo.clientId
  );

  if (!clientInfo) {
    return c.text("Invalid client_id", 400);
  }

  // For this demo, we'll show a simple HTML approval page
  // In a real implementation, you might:
  // - Check cookies to see if this client was previously approved
  // - Redirect to an external OAuth provider (GitHub, Google, etc.)
  // - Show a custom approval UI
  const approvalPage = `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Authorize ${clientInfo.clientName || "MCP Client"}</title>
        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
            line-height: 1.6;
          }
          .card {
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 30px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
          }
          h1 { margin-top: 0; }
          .client-info {
            background: #f5f5f5;
            padding: 15px;
            border-radius: 4px;
            margin: 20px 0;
          }
          .actions {
            display: flex;
            gap: 10px;
            margin-top: 20px;
          }
          button {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
          }
          .approve {
            background: #0070f3;
            color: white;
            flex: 1;
          }
          .deny {
            background: #eee;
            color: #333;
          }
        </style>
      </head>
      <body>
        <div class="card">
          <h1>Authorization Request</h1>
          <p><strong>${clientInfo.clientName || "An MCP Client"}</strong> is requesting access to the MCP server.</p>

          <div class="client-info">
            <p><strong>Client ID:</strong> ${clientInfo.clientId}</p>
            ${clientInfo.clientUri ? `<p><strong>Website:</strong> <a href="${clientInfo.clientUri}" target="_blank">${clientInfo.clientUri}</a></p>` : ""}
            <p><strong>Requested Scopes:</strong> ${oauthReqInfo.scope.join(", ") || "none"}</p>
          </div>

          <p>If you approve, this client will be able to:</p>
          <ul>
            <li>Access MCP tools on your behalf</li>
            <li>Receive your authenticated user information</li>
          </ul>

          <form method="POST" action="/authorize">
            <input type="hidden" name="state" value="${btoa(JSON.stringify(oauthReqInfo))}">
            <div class="actions">
              <button type="button" class="deny" onclick="window.history.back()">Cancel</button>
              <button type="submit" class="approve">Approve</button>
            </div>
          </form>
        </div>
      </body>
    </html>
  `;

  return c.html(approvalPage);
});

/**
 * POST /authorize - Handle authorization approval
 *
 * This endpoint is called when the user approves the authorization.
 * It completes the OAuth flow by creating a grant and redirecting back to the client.
 */
app.post("/authorize", async (c) => {
  const formData = await c.req.formData();
  const state = formData.get("state");

  if (!state || typeof state !== "string") {
    return c.text("Missing state parameter", 400);
  }

  let oauthReqInfo: AuthRequest;
  try {
    oauthReqInfo = JSON.parse(atob(state));
  } catch {
    return c.text("Invalid state parameter", 400);
  }

  // For this demo, we'll use a static user ID
  // In a real implementation, you would:
  // 1. Have already authenticated the user (via external OAuth, session, etc.)
  // 2. Use their actual user ID and profile information
  const userId = "demo-user";
  const userProfile = {
    userId: "demo-user",
    username: "Demo User",
    email: "demo@example.com"
  };

  // Complete the authorization by creating a grant
  const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
    request: oauthReqInfo,
    userId: userId,
    metadata: {
      label: "MCP Server Access",
      clientName:
        (await c.env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId))
          ?.clientName || "Unknown Client"
    },
    scope: oauthReqInfo.scope,
    props: userProfile
  });

  // Redirect back to the client with the authorization code
  return c.redirect(redirectTo, 302);
});

/**
 * GET / - Home page
 *
 * Shows information about the OAuth server
 */
app.get("/", (c) => {
  return c.html(`
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>MCP OAuth Server</title>
        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
            line-height: 1.6;
          }
          h1 { color: #0070f3; }
          .endpoint {
            background: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
            font-family: monospace;
          }
        </style>
      </head>
      <body>
        <h1>MCP OAuth Server</h1>
        <p>This is an authenticated MCP server that uses OAuth 2.1 for authorization.</p>

        <h2>Available Endpoints</h2>
        <div class="endpoint">/mcp - MCP server endpoint (requires Bearer token)</div>
        <div class="endpoint">/authorize - OAuth authorization endpoint</div>
        <div class="endpoint">/token - OAuth token endpoint</div>
        <div class="endpoint">/register - Client registration endpoint</div>
        <div class="endpoint">/.well-known/oauth-authorization-server - OAuth metadata</div>

        <h2>Getting Started</h2>
        <p>To connect to this MCP server:</p>
        <ol>
          <li>Register your MCP client via the <code>/register</code> endpoint</li>
          <li>Initiate the OAuth flow by redirecting to <code>/authorize</code></li>
          <li>Exchange the authorization code for an access token at <code>/token</code></li>
          <li>Use the access token to access the <code>/mcp</code> endpoint</li>
        </ol>
      </body>
    </html>
  `);
});

export { app as AuthHandler };