From 1a20477c1473e015f63e34929ca18a22f02fa7d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 18:33:55 +0000 Subject: [PATCH] Add RFC 7591 dynamic client registration to MCP OAuth proxy Remove hardcoded clientId from .mcp.json so clients (Claude Code) can dynamically register via POST /oauth/register instead of requiring a pre-configured client ID. The MCP server generates a UUID per client and stores it in-memory; the real Entra ID credentials stay server-side. https://claude.ai/code/session_01S8iM5fK53kYa54tydpgsNq --- .mcp.json | 5 +-- .../autopilot-monitor-mcp/src/oauth.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.mcp.json b/.mcp.json index 3f584df6..37ac1740 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,10 +2,7 @@ "mcpServers": { "autopilot-monitor": { "type": "http", - "url": "https://autopilotmonitor-mcp.kindwave-58b4b547.westeurope.azurecontainerapps.io/mcp", - "oauth": { - "clientId": "1a400946-62c1-4ab4-aa37-f730ac89704d" - } + "url": "https://autopilotmonitor-mcp.kindwave-58b4b547.westeurope.azurecontainerapps.io/mcp" } } } diff --git a/src/McpServer/autopilot-monitor-mcp/src/oauth.ts b/src/McpServer/autopilot-monitor-mcp/src/oauth.ts index 290257c6..c74382ef 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/oauth.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/oauth.ts @@ -12,12 +12,20 @@ * 4. Claude Code → POST /oauth/token → proxied to Entra ID /token */ import { Router } from 'express'; +import crypto from 'node:crypto'; const CLIENT_ID = process.env.AUTOPILOT_ENTRA_CLIENT_ID ?? '1a400946-62c1-4ab4-aa37-f730ac89704d'; const CLIENT_SECRET = process.env.AUTOPILOT_ENTRA_CLIENT_SECRET ?? ''; const AUTHORITY = process.env.AUTOPILOT_ENTRA_AUTHORITY ?? 'https://login.microsoftonline.com/organizations'; const SCOPES = `api://${CLIENT_ID}/access_as_user openid profile offline_access`; +/** + * In-memory store for dynamically registered OAuth clients (RFC 7591). + * Clients re-register after a server restart — this is fine because the MCP + * server acts as an OAuth proxy and the real credentials live in Entra ID. + */ +const registeredClients = new Map(); + /** * Derives the public base URL of this MCP server. * In production: from X-Forwarded-Host / X-Forwarded-Proto (set by Container Apps ingress). @@ -41,6 +49,7 @@ export function createOAuthRouter(): Router { issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/authorize`, token_endpoint: `${baseUrl}/oauth/token`, + registration_endpoint: `${baseUrl}/oauth/register`, response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], code_challenge_methods_supported: ['S256'], @@ -48,6 +57,28 @@ export function createOAuthRouter(): Router { }); }); + // --- Dynamic Client Registration (RFC 7591) --- + router.post('/oauth/register', (req, res) => { + const { client_name, redirect_uris, grant_types, response_types, token_endpoint_auth_method } = req.body ?? {}; + + const clientId = crypto.randomUUID(); + registeredClients.set(clientId, { + name: client_name ?? 'unknown', + redirectUris: Array.isArray(redirect_uris) ? redirect_uris : [], + }); + + console.error(`[oauth/register] Registered dynamic client ${clientId} (${client_name ?? 'unknown'})`); + + res.status(201).json({ + client_id: clientId, + client_name: client_name ?? 'unknown', + redirect_uris: redirect_uris ?? [], + grant_types: grant_types ?? ['authorization_code', 'refresh_token'], + response_types: response_types ?? ['code'], + token_endpoint_auth_method: token_endpoint_auth_method ?? 'none', + }); + }); + // --- Authorize: redirect to Entra ID --- router.get('/oauth/authorize', (req, res) => { const baseUrl = getPublicBaseUrl(req);