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:
Chris Coutinho
2026-03-02 17:22:23 +01:00
parent 0a53aa5fcd
commit f43343356e
17 changed files with 727 additions and 247 deletions
+11 -2
View File
@@ -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),
}
)