fix: address review feedback — security, caching, CI 429 retry
- Add 429 retry with exponential backoff to register_client() (fixes CI oauth matrix failures from parallel DCR requests) - Make client_id, redirect_uri, and PKCE mandatory at token endpoint - Add null-checks for discovery_url and OAuth credentials in proxy flows - Add OIDC discovery document caching with 5-min TTL - Add per-IP rate limiting on /oauth/register DCR proxy - Discover DCR endpoint from OIDC discovery instead of hardcoding - Extract extract_user_id_from_token to auth/token_utils.py (breaks circular imports between server/ and auth/ layers) - Add TTL scope cache in scope_authorization.py (avoids DB hit per tool) - Add defense-in-depth scope validation in storage layer - Broaden elicitation exception handling with graceful fallback - Add idempotentHint to nc_auth_check_status, return "pending" status after accepted elicitation, add polling interval to description - Change ALL_SUPPORTED_SCOPES from tuple to frozenset for O(1) lookups - Replace Optional[str] with str | None throughout config.py - Use default_factory for ProxyCodeEntry/ASProxySession dataclasses - Add proxy code/session cleanup to background loop - Fix OIDC verification CI step to only run for oauth/login-flow modes - Add unit tests for access.py REST endpoints (10 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0a53aa5fcd
commit
f43343356e
@@ -1,6 +1,7 @@
|
||||
"""Scope-based authorization for MCP tools."""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
@@ -141,7 +142,7 @@ def require_scopes(*required_scopes: str):
|
||||
if get_settings().enable_login_flow and not set(required_scopes).issubset(
|
||||
IDENTITY_ONLY_SCOPES
|
||||
):
|
||||
from nextcloud_mcp_server.server.oauth_tools import ( # noqa: PLC0415
|
||||
from nextcloud_mcp_server.auth.token_utils import ( # noqa: PLC0415
|
||||
extract_user_id_from_token,
|
||||
)
|
||||
|
||||
@@ -476,9 +477,18 @@ def discover_all_scopes(mcp) -> list[str]:
|
||||
|
||||
# ── Login Flow v2 helpers ────────────────────────────────────────────────
|
||||
|
||||
# Scope cache: user_id → (expires_at, scopes)
|
||||
_scope_cache: dict[str, tuple[float, list[str] | str | None]] = {}
|
||||
_SCOPE_CACHE_TTL = 300 # 5 minutes
|
||||
|
||||
|
||||
def invalidate_scope_cache(user_id: str) -> None:
|
||||
"""Remove cached scopes for a user (call when scopes are updated)."""
|
||||
_scope_cache.pop(user_id, None)
|
||||
|
||||
|
||||
async def _get_stored_scopes(user_id: str) -> list[str] | str | None:
|
||||
"""Look up stored app password scopes for a user.
|
||||
"""Look up stored app password scopes for a user (with TTL cache).
|
||||
|
||||
Returns:
|
||||
- list[str]: Specific scopes granted
|
||||
@@ -489,11 +499,21 @@ async def _get_stored_scopes(user_id: str) -> list[str] | str | None:
|
||||
Storage/infrastructure exceptions propagate to the caller
|
||||
(require_scopes decorator) for proper MCP error responses.
|
||||
"""
|
||||
now = time.time()
|
||||
if user_id in _scope_cache:
|
||||
expires_at, cached = _scope_cache[user_id]
|
||||
if now < expires_at:
|
||||
return cached
|
||||
|
||||
storage = await get_shared_storage()
|
||||
|
||||
data = await storage.get_app_password_with_scopes(user_id)
|
||||
if data is None:
|
||||
return None
|
||||
if data["scopes"] is None:
|
||||
return "all"
|
||||
return data["scopes"]
|
||||
result = None
|
||||
elif data["scopes"] is None:
|
||||
result = "all"
|
||||
else:
|
||||
result = data["scopes"]
|
||||
|
||||
_scope_cache[user_id] = (now + _SCOPE_CACHE_TTL, result)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user