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:
@@ -14,6 +14,7 @@ from nextcloud_mcp_server.api.passwords import (
|
||||
_extract_basic_auth,
|
||||
_get_app_password_storage,
|
||||
)
|
||||
from nextcloud_mcp_server.auth.scope_authorization import invalidate_scope_cache
|
||||
from nextcloud_mcp_server.models.auth import ALL_SUPPORTED_SCOPES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -79,6 +80,11 @@ async def update_user_scopes(request: Request) -> JSONResponse:
|
||||
|
||||
This only updates the stored scopes, not the app password itself.
|
||||
The app password remains valid; scope enforcement is application-level.
|
||||
|
||||
Security note: This endpoint allows direct scope modification without
|
||||
re-authenticating via Login Flow. The caller must authenticate with
|
||||
valid BasicAuth credentials (user_id + app_password), which serves
|
||||
as the authorization check.
|
||||
"""
|
||||
path_user_id = request.path_params.get("user_id")
|
||||
if not path_user_id:
|
||||
@@ -113,7 +119,7 @@ async def update_user_scopes(request: Request) -> JSONResponse:
|
||||
{
|
||||
"success": False,
|
||||
"error": f"Invalid scopes: {', '.join(invalid)}",
|
||||
"valid_scopes": ALL_SUPPORTED_SCOPES,
|
||||
"valid_scopes": sorted(ALL_SUPPORTED_SCOPES),
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
@@ -137,6 +143,9 @@ async def update_user_scopes(request: Request) -> JSONResponse:
|
||||
scopes=scopes,
|
||||
)
|
||||
|
||||
# Invalidate scope cache so subsequent tool calls see updated scopes
|
||||
invalidate_scope_cache(username)
|
||||
|
||||
return JSONResponse(
|
||||
{
|
||||
"success": True,
|
||||
@@ -159,6 +168,6 @@ async def list_supported_scopes(_: Request) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
{
|
||||
"success": True,
|
||||
"scopes": ALL_SUPPORTED_SCOPES,
|
||||
"scopes": sorted(ALL_SUPPORTED_SCOPES),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user