feat(mcp): granular OAuth scopes and per-client rate limiting

- Split `media:read` into `geo:read` and `weather:read` scopes
- Add dedicated `atlas:read/write` scopes (previously under `places`)
- Add dedicated `todos:read/write` scopes (previously under `collab`)
- Rate limiting now keyed by userId+clientId instead of userId alone
- Bind MCP sessions to the OAuth client that created them
- Log MCP tool calls to audit log with clientId
- Invalidate all MCP sessions on addon state change
- Reduce session sweep interval from 10min to 1min
- Update all translations with new scope labels
This commit is contained in:
jubnl
2026-04-11 02:06:09 +02:00
parent 4670d4914c
commit 535c06bb3f
39 changed files with 1930 additions and 237 deletions
+9 -2
View File
@@ -193,8 +193,11 @@ oauthPublicRouter.post('/oauth/register', dcrLimiter, (req: Request, res: Respon
const authMethod = typeof body.token_endpoint_auth_method === 'string' ? body.token_endpoint_auth_method : 'client_secret_post';
const isPublic = authMethod === 'none';
// Resolve requested scopes — default to all supported scopes if not specified
const rawScope = typeof body.scope === 'string' ? body.scope : ALL_SCOPES.join(' ');
// Resolve requested scopes — scope is required; no implicit full-access grant
if (typeof body.scope !== 'string' || body.scope.trim() === '') {
return res.status(400).json({ error: 'invalid_client_metadata', error_description: 'scope is required' });
}
const rawScope = body.scope;
const requestedScopes = rawScope.split(' ').filter(s => (ALL_SCOPES as string[]).includes(s));
if (requestedScopes.length === 0) {
return res.status(400).json({ error: 'invalid_client_metadata', error_description: 'No valid scopes requested' });
@@ -351,6 +354,10 @@ oauthApiRouter.post('/authorize', requireCookieAuth, (req: Request, res: Respons
codeChallengeMethod: 'S256',
});
if (!code) {
return res.status(503).json({ error: 'server_error', error_description: 'Authorization server is temporarily unavailable' });
}
const url = new URL(redirect_uri);
url.searchParams.set('code', code);
if (state) url.searchParams.set('state', state);