fix(mcp): narrow OAuth scope to allowed intersection instead of rejecting

When a client requests scopes it is not permitted for, silently drop
them rather than failing the entire authorization flow. The token is
issued with only the intersection of requested and allowed scopes.

Also fix /authorize/validate to always return HTTP 200 so the consent
page can surface the actual error_description instead of a generic
axios failure message.
This commit is contained in:
jubnl
2026-04-09 23:47:53 +02:00
parent 54f280c366
commit 5b44fe68b1
3 changed files with 50 additions and 24 deletions
+8 -6
View File
@@ -425,27 +425,29 @@ export function validateAuthorizeRequest(
}
const allowedScopes: string[] = JSON.parse(client.allowed_scopes);
const disallowed = requestedScopes.filter(s => !allowedScopes.includes(s));
if (disallowed.length > 0) {
return { valid: false, error: 'invalid_scope', error_description: `Scopes not permitted for this client: ${disallowed.join(', ')}` };
// Narrow to the intersection: drop scopes the client isn't permitted for rather
// than rejecting the whole request (per OAuth 2.0 §3.3 scope narrowing).
const grantedScopes = requestedScopes.filter(s => allowedScopes.includes(s));
if (grantedScopes.length === 0) {
return { valid: false, error: 'invalid_scope', error_description: 'None of the requested scopes are permitted for this client' };
}
if (userId === null) {
return {
valid: true,
client: { name: client.name, allowed_scopes: allowedScopes },
scopes: requestedScopes,
scopes: grantedScopes,
loginRequired: true,
};
}
const existingConsent = getConsent(params.client_id, userId);
const consentRequired = !existingConsent || !isConsentSufficient(existingConsent, requestedScopes);
const consentRequired = !existingConsent || !isConsentSufficient(existingConsent, grantedScopes);
return {
valid: true,
client: { name: client.name, allowed_scopes: allowedScopes },
scopes: requestedScopes,
scopes: grantedScopes,
consentRequired,
};
}