fix(oauth): allow generic RFC 7591 DCR + expand install snippets
All checks were successful
Deploy to Production / deploy (push) Successful in 1m28s
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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user