From f7c9f3dc94d04dd39d846fc8860cf17147966031 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 4 Apr 2026 11:17:21 +0200 Subject: [PATCH] fix: add Protected Resource Metadata endpoint (RFC 9728) The MCP auth spec (2025-06-18+) requires /.well-known/oauth-protected-resource in addition to /.well-known/oauth-authorization-server. Claude fetches the protected resource metadata first after receiving a 401, then discovers the authorization server from it. Also fixes WWW-Authenticate header to use absolute URL pointing to the protected resource endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) --- check-oauth.sh | 8 ++++++++ src/server/mcp/index.ts | 8 +++++--- src/server/routes/oauth.ts | 10 ++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100755 check-oauth.sh diff --git a/check-oauth.sh b/check-oauth.sh new file mode 100755 index 0000000..dd9886e --- /dev/null +++ b/check-oauth.sh @@ -0,0 +1,8 @@ +#!/bin/bash +docker exec gearbox bun -e " +const{Database}=require('bun:sqlite'); +const db=new Database('./data/gearbox.db'); +console.log('CLIENTS:', JSON.stringify(db.query('SELECT * FROM oauth_clients ORDER BY id DESC LIMIT 5').all(), null, 2)); +console.log('CODES:', JSON.stringify(db.query('SELECT id,client_id,used,expires_at FROM oauth_codes ORDER BY id DESC LIMIT 5').all(), null, 2)); +console.log('TOKENS:', JSON.stringify(db.query('SELECT id,client_id,expires_at FROM oauth_tokens ORDER BY id DESC LIMIT 5').all(), null, 2)); +" diff --git a/src/server/mcp/index.ts b/src/server/mcp/index.ts index 6235cc3..1ec4ab0 100644 --- a/src/server/mcp/index.ts +++ b/src/server/mcp/index.ts @@ -115,10 +115,12 @@ mcpRoutes.use("/*", async (c, next) => { return c.json({ error: "Invalid API key" }, 401); } - // No auth provided — return 401 with WWW-Authenticate to trigger OAuth flow + // No auth provided — return 401 with WWW-Authenticate to trigger OAuth flow (RFC 9728) + const baseUrl = ( + process.env.GEARBOX_URL || new URL(c.req.url).origin + ).replace(/\/$/, ""); return c.text("Unauthorized", 401, { - "WWW-Authenticate": - 'Bearer resource_metadata="/.well-known/oauth-authorization-server"', + "WWW-Authenticate": `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`, }); }); diff --git a/src/server/routes/oauth.ts b/src/server/routes/oauth.ts index 87a4ac9..d2b1927 100644 --- a/src/server/routes/oauth.ts +++ b/src/server/routes/oauth.ts @@ -84,6 +84,16 @@ function renderLoginForm(params: { export const wellKnownRoute = new Hono(); +// Protected Resource Metadata (RFC 9728) — Claude fetches this first after 401 +wellKnownRoute.get("/oauth-protected-resource", (c) => { + const baseUrl = getBaseUrl(c); + return c.json({ + resource: `${baseUrl}/mcp`, + authorization_servers: [baseUrl], + }); +}); + +// OAuth Authorization Server Metadata (RFC 8414) — Claude fetches this second wellKnownRoute.get("/oauth-authorization-server", (c) => { const baseUrl = getBaseUrl(c); return c.json({