10 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.7 ce80a36877 fix(auth): address PR #757 round-3 review feedback
Three review items from the third-round review on PR #757:

- scope_authorization: split the combined logger.warning(error_msg) in
  the require_scopes decorator's missing-app-password branch into two
  lazy %-style logger calls (one per branch), keeping the f-string
  error_msg for the exception only. The else branch also logs the
  elicit_result for diagnostics. Bypassing lazy %-interpolation in
  security-sensitive code formatted the message regardless of log level
  and matched the repo-wide lazy-logging preference; the new code now
  conforms.
- config + browser_oauth_routes: wire COOKIE_SECURE through Settings
  (cookie_secure: bool | None = None) so _should_use_secure_cookies()
  reads it via get_settings() rather than os.getenv. Completes the
  consolidation pass that touched this file in commit 7464340 and
  removes the last raw os.getenv from browser_oauth_routes.py
  (import os dropped). Dynaconf auto-coerces "true"/"false" → bool;
  "1"/"0" arrive as int and are normalised by an explicit bool() at
  the consumer.
- elicitation: clarify the _astrolabe_settings_url docstring to call
  out that the empty-string case is also a None-return path (matches
  the existing `if not base:` guard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:24:50 +02:00
Chris CoutinhoandClaude Opus 4.7 7464340763 fix(auth): address PR #757 round-2 review feedback
Four review items from the second-round review on PR #757:

- scope_authorization: broaden the post-elicit retry message to acknowledge
  the 5-minute scope-cache TTL — if the LFv2 poller is still in-flight at
  acknowledge-time, the immediate retry can still hit a stale cache.
- elicitation: extract a shared `_run_elicit(ctx, message, schema, *,
  log_label)` helper so `present_login_url` and
  `present_provisioning_required` no longer duplicate the
  hasattr-guard / try-NotImplementedError / try-Exception fallback block.
  The data-acknowledged warning specific to login-flow stays in
  `present_login_url` so behaviour is preserved exactly.
- elicitation: detect missing http:// / https:// scheme in
  `_astrolabe_settings_url`, log a warning, and return None — caller
  renders the safe tool-only fallback instead of producing a broken link.
  New unit test locks this in.
- browser_oauth_routes: replace the stray
  `os.getenv(\"NEXTCLOUD_HOST\")` in `_should_use_secure_cookies` with
  `get_settings().nextcloud_host` for consistency with the rest of the
  file (PR #757 review nit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:33:43 +02:00
Chris CoutinhoandClaude Opus 4.7 f3256e515e refactor(config): consolidate NEXTCLOUD_PUBLIC_ISSUER_URL through Settings
Lift NEXTCLOUD_PUBLIC_ISSUER_URL out of raw os.getenv reads into
Settings.nextcloud_public_issuer_url across all 8 production call sites
(app.py x2, oauth_routes.py x2, browser_oauth_routes.py,
provision_routes.py, userinfo_routes.py, elicitation.py). cli.py
remains the env-write source so the existing config-by-flag pipeline
still works.

Also addresses remaining PR #757 review nits:
- elicitation.py: align URL-present/absent wording on "open in your
  browser" so users don't try clicking in the terminal
- test_scope_authorization_stored.py: lock in the deliberately-shared
  fall-through branch with explicit declined/cancelled decorator tests
- test_elicitation.py: switch from monkeypatch.setenv to
  patch(get_settings) since Settings is now the canonical surface

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:16:19 +02:00
Chris CoutinhoandClaude Opus 4.7 822a8fe2ed fix(auth): address PR #757 review feedback
- Branch the ProvisioningRequiredError message on the elicit result so a
  user who acknowledged the prompt isn't told to call
  nc_auth_provision_access (which would loop an LLM that just confirmed
  via elicitation). Other paths keep the existing instruction.
- Convert present_login_url's f-string logger.warning to lazy %s, matching
  present_provisioning_required and the repo's lazy-logging preference.
- Add a test for NEXTCLOUD_PUBLIC_ISSUER_URL trailing-slash normalization.
- Strengthen the decorator-elicits test: split into the "accepted" and
  "message_only" branches so the error-message change is regression-tested.

Refs: cbcoutinho/nextcloud-mcp-server#757#issuecomment-4363552487

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:34:56 +02:00
Chris CoutinhoandClaude Opus 4.7 2da8b38aeb feat(auth): elicit Astrolabe URL on missing app password
When a tool requiring Nextcloud access is called without a stored app
password (Login Flow v2 mode), the @require_scopes decorator now invokes
MCP elicitation with a clickable Astrolabe settings URL — reconstructed
from NEXTCLOUD_PUBLIC_ISSUER_URL / NEXTCLOUD_HOST — before raising
ProvisioningRequiredError. Clients without elicitation support fall back
to the existing text error.

Surfaced by cbcoutinho/nextcloud-mcp-server#752, where users hit a 401
after OAuth and had no clickable URL to start Login Flow v2 from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:02:18 +02:00
Chris CoutinhoandClaude Opus 4.6 f43343356e 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>
2026-03-02 17:22:23 +01:00
Chris CoutinhoandClaude Opus 4.6 0d14c75eb1 fix: address remaining PR #589 review findings
- Consolidate MCP session + login flow cleanup into _mcp_session_with_login_flow() helper,
  replacing 4 duplicated AsyncExitStack sites in app.py
- Fix get_shared_storage() race condition by using module-level anyio.Lock() init
  (reverts regression from ba59763)
- Collapse cosmetic if/else branching in scope_authorization.py
- Consolidate dual password storage paths into single store_app_password_with_scopes() call
- Mark unused request param as _ in list_supported_scopes
- Make ALL_SUPPORTED_SCOPES an immutable tuple; use list() instead of .copy()
- Add hasattr(ctx, "elicit") guard in elicitation.py, narrow except to NotImplementedError
- Add YAML comment explaining --oauth flag for mcp-login-flow service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:59:56 +01:00
Chris CoutinhoandClaude Opus 4.6 ba597634bd fix: address PR #589 review findings
- Fix anyio.Lock() created at module import time; use lazy init in
  get_shared_storage() to avoid instantiation before event loop exists
- Stop get_login_flow_session from silently swallowing DB exceptions;
  re-raise and handle in caller with proper error response
- Update ProvisionAccessResponse and UpdateScopesResponse status field
  docs to include all actual values (declined, cancelled, unchanged)
- Narrow except clause in present_login_url to (AttributeError,
  NotImplementedError) instead of bare Exception
- Add KeyError handling in LoginFlowV2Client.initiate() and poll() for
  clear errors on malformed Nextcloud responses
- Simplify redundant env-var bypass branches in scope_authorization.py
- Extract _maybe_login_flow_cleanup() context manager to replace 4
  inline cleanup loop registrations in app.py; move sleep to end of
  loop body so cleanup runs once at startup
- Replace fragile string replacement in _rewrite_login_flow_url with
  proper urllib.parse URL handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:10:57 +01:00
Chris CoutinhoandClaude Opus 4.6 1a6ce0fa7d fix: address PR review issues for Login Flow v2
- Fix circular dependency in scope_authorization: auth tools requiring
  only identity scopes (openid/profile/email) now bypass the login flow
  provisioning check, so unprovisioned users can call provisioning tools
- Fix no-op detection in nc_auth_update_scopes: NULL scopes (legacy "all")
  now correctly map to ALL_SUPPORTED_SCOPES instead of empty set
- Fix get_app_password_with_scopes swallowing exceptions: re-raise instead
  of returning None, matching sibling methods
- Add missing audit logging to update_app_password_scopes,
  delete_login_flow_session, and delete_expired_login_flow_sessions
- Pin setup-uv to v7.3.1 in CI unit-test job (was v7.3.0)
- Add FastMCP type annotation to register_auth_tools parameter
- Log warning when user accepts elicitation without checking acknowledged box

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:02:30 +01:00
Chris CoutinhoandClaude Opus 4.6 8b5c2395b5 feat: add Docker Compose profiles and Login Flow v2 service
Add selective service startup via Docker Compose profiles so each MCP
deployment mode runs independently. Also add the new mcp-login-flow
service (port 8004) for Login Flow v2 authentication (ADR-022).

Profile assignments:
- single-user: mcp (port 8000)
- multi-user-basic: mcp-multi-user-basic (port 8003)
- oauth: mcp-oauth (port 8001)
- keycloak: keycloak + mcp-keycloak (port 8002)
- login-flow: mcp-login-flow (port 8004)

Infrastructure services (db, redis, app, recipes) always start.

Integration tests cover the full Login Flow v2 provisioning flow:
OAuth → browser login → app password → Nextcloud API access for
notes, calendar, contacts, files, deck, and cookbook operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:33:54 +01:00