fix: add Protected Resource Metadata endpoint (RFC 9728)
All checks were successful
CI / ci (push) Successful in 29s
CI / e2e (push) Successful in 1m1s

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 11:17:21 +02:00
parent b71833ef79
commit f7c9f3dc94
3 changed files with 23 additions and 3 deletions

8
check-oauth.sh Executable file
View File

@@ -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));
"

View File

@@ -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"`,
});
});

View File

@@ -84,6 +84,16 @@ function renderLoginForm(params: {
export const wellKnownRoute = new Hono<Env>();
// 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({