- Flow 2 (oauth_authorize_nextcloud) now generates a nonce, stores it on
the oauth_session row, forwards it to the IdP, and verifies it via
expected_nonce in oauth_callback_nextcloud — closes the last replay-
protection gap (round-3 finding 1).
- _origin_matches_self fails closed when mcp_server_url is missing
instead of allowing the logout, and the diagnostic log is promoted
from warning to error so the misconfiguration is monitorable
(round-3 finding 2). New regression test pins the new behaviour.
- The five user_id-accepting helpers in oauth_tools.py (get_provisioning_status,
provision_nextcloud_access, revoke_nextcloud_access, check_provisioning_status,
check_logged_in) are renamed with leading underscores to make the
trust boundary structural rather than documentary
(round-3 finding 3).
- create_browser_session and delete_browser_session now emit audit_log
rows so session establishment / teardown match the pattern used by
the rest of the security-relevant storage operations
(round-3 nit 5). delete_browser_session selects user_id before delete
so the audit row is attributable.
- oauth_login_callback no longer reflects raw IdP-error text or
exception strings into the HTML failure page; users see a generic
"internal error occurred" message + a correlation ID, with the
detail logged server-side keyed by the same ID (round-3 nit 6).
The XSS regression test is updated to pin the stricter contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- oauth_login_callback's integrated-mode token-exchange branch now reuses
the shared discovery cache via get_oidc_discovery (round-2 finding 1).
- AS proxy flow now generates an OIDC nonce in oauth_authorize, stores it
on ASProxySession, forwards it to the IdP, and passes it as
expected_nonce to verify_id_token in _oauth_callback_as_proxy
(round-2 finding 2).
- Consolidate the two parallel discovery caches: oauth_routes' local
_discovery_cache and _get_cached_discovery are removed; all callers
now go through token_utils.get_oidc_discovery, which acquires the
follow_redirects=True knob it needs for Nextcloud installs without
pretty URLs (round-2 finding 3).
- Demote per-user INFO logs in oauth_tools.py (check_logged_in,
get_provisioning_status) to DEBUG; the elicitation auth URL is no
longer logged because it contains a sensitive state token
(round-2 finding 4).
Also pin nonce binding behaviour with a new unit test that asserts
_oauth_callback_as_proxy forwards session.nonce to verify_id_token, and
update test mocks to track the cache consolidation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses all 9 findings from the review on PR #758:
Blocking:
- _revoke_refresh_token_at_idp now reads config from oauth_ctx["config"]
(the production-shaped nested dict). Previously read flat keys, causing
IdP revocation to silently no-op in production. Test fixtures rebuilt
to the realistic nested shape so the bug can't regress unnoticed.
- HTML error responses in oauth_login_callback now wrap IdP-controlled
error_body, str(e), and the attacker-controlled error/error_description
query params in html_escape. New test_browser_oauth_xss.py pins this.
Important:
- New _safe_next_url helper validates the ?next= query param at write
time (oauth_login), in oauth_logout, and on read from the session row
in oauth_login_callback. Blocks https://, // (protocol-relative), and
CRLF/whitespace injection.
- verify_id_token now caches discovery + JWKS (5-min TTL) using the
same pattern as oauth_routes._get_cached_discovery. New caching
regression test pins to one fetch per URL across multiple calls.
- /oauth/logout is now POST-only at the route layer (defeats passive
CSRF via <img src>). oauth_logout also validates Origin/Referer
against the configured mcp_server_url. Logout UI in user_info.html
converted from <a href> to <form method="post">.
- New storage.cleanup_expired_browser_sessions() called from the hourly
cleanup loop in app.py — previously these rows accumulated for users
who never explicitly logged out.
Nits:
- Demoted INFO logs that leaked oauth_config.keys() / client_id /
token-storage state to DEBUG. Operator-relevant outcome lines
(login successful, refresh token stored, logged out) stay INFO.
- verify_id_token algorithms widened to RS256, PS256, ES256 — covers
Azure AD (PS256) and Cognito/some Keycloak realms (ES256). Symmetric
and "none" remain off the allowlist.
- Migrated all Optional[X] usages in auth/storage.py to X | None per
CLAUDE.md.
Breaking change: GET /oauth/logout now returns 405. The in-tree logout
UI was migrated to a POST form; any external bookmark or curl-based
caller that relied on GET will need to switch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>