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
This commit is contained in:
Claude
2026-03-28 18:33:55 +00:00
parent 8aca182b35
commit 1a20477c14
2 changed files with 32 additions and 4 deletions
+1 -4
View File
@@ -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"
}
}
}
@@ -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<string, { name: string; redirectUris: string[] }>();
/**
* 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);