fix(oauth): allow generic RFC 7591 DCR + expand install snippets
All checks were successful
Deploy to Production / deploy (push) Successful in 1m28s

- /oauth/register: drop resource_required check, accept generic
  registrations (Claude Desktop omits resource in DCR body per spec).
  serverId stored as NULL; /authorize still enforces org-ownership
  + access-token aud claim still pinned to resource. Fixes Claude
  Desktop DCR failure (ofid_d7e39530c109fa7f).
- /oauth/authorize: skip strict server.id check when client.serverId
  is NULL (generic client); org check remains the security boundary.
- schema: oauth_clients.server_id no longer NOT NULL.
- migration 0002: ALTER COLUMN server_id DROP NOT NULL (already
  applied on prod).
- install-snippets: add Claude Code (CLI), VS Code, Codex, raw URL
  tabs. Claude Desktop now shows form-field values (Name / Remote MCP
  Server URL / OAuth Client ID / Secret) matching the new Custom
  Connector UI instead of the obsolete JSON config.
- types: InstallTarget enum extended.
- hero-video: clicking the audio toggle restarts the video from
  frame 0 so unmute aligns with the spoken opening.
- marketing: drop em-dashes from rendered copy.
This commit is contained in:
Marco Sadjadi
2026-05-28 17:20:01 +02:00
parent e75f9ad4fe
commit 3a05766f88
9 changed files with 172 additions and 38 deletions

View File

@@ -103,13 +103,20 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
const parsed = Body.safeParse(req.body);
if (!parsed.success) return reply.code(400).send({ error: 'invalid_request' });
// RFC 7591 makes `resource` optional in the registration request body.
// Claude Desktop and several other MCP clients perform a generic
// registration first and only declare the resource later during the
// authorization request (RFC 8707). When a resource is provided we
// bind the client to that server; otherwise we accept a generic
// registration and let /oauth/authorize enforce the resource → org
// check on every authorization. The token endpoint additionally
// pins the audience claim to the resource, so a generic client still
// can't mint a token usable against a server the user does not own.
let serverId: string | null = null;
if (parsed.data.resource) {
const server = await resolveServerByResource(parsed.data.resource);
if (!server) return reply.code(400).send({ error: 'invalid_resource' });
serverId = server.id;
} else {
return reply.code(400).send({ error: 'resource_required' });
}
const clientId = `bmm_${crypto.randomBytes(12).toString('hex')}`;
@@ -166,7 +173,16 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
if (!redirectOk) return reply.code(400).send({ error: 'invalid_redirect_uri' });
const server = await resolveServerByResource(parsed.data.resource);
if (!server || server.id !== client.serverId) {
if (!server) {
return reply.code(400).send({ error: 'invalid_resource' });
}
// Clients that registered against a specific server (`client.serverId`
// set) must keep authorizing against the same one. Clients that
// registered generically (`client.serverId === null`, e.g. Claude
// Desktop after RFC 7591 DCR) can authorize against any server the
// logged-in user actually owns — the org check below is the real
// boundary.
if (client.serverId !== null && server.id !== client.serverId) {
return reply.code(400).send({ error: 'invalid_resource' });
}
if (server.orgId !== user.orgId) {