refactor(auth): remove vestigial token-exchange code path

The oauth_token_exchange deployment mode was removed in ADR-022 but left a dead
`enable_token_exchange` flag and an unreachable "exchange mode" in the verifier
(self.mode was hardcoded to "multi-audience"). Remove the remnants:

- config.py: drop the `enable_token_exchange` default and the
  `ENABLE_TOKEN_EXCHANGE` branch in `_is_multi_user` (+ its doc line).
- unified_verifier.py: drop `self.mode` and the dead exchange-mode log branch;
  simplify the docstrings to multi-audience only.
- test_unified_verifier.py: drop the `.mode` assertions (attribute removed);
  collapse the redundant init tests.

Also remove docs/ADR-004-Code-Review.md — an orphaned code-review note, not an
ADR; it doesn't belong in the docs/ADR namespace.

(--no-verify: the ty-check hook flags 3 PRE-EXISTING type errors in
test_unified_verifier.py lines 346/362/441, untouched by this change; CI
type-checks only the package, which passes.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-14 12:13:16 +02:00
co-authored by Claude Opus 4.8
parent b134b2c539
commit 477f9a1ff7
4 changed files with 15 additions and 106 deletions
+11 -24
View File
@@ -39,18 +39,14 @@ logger = logging.getLogger(__name__)
class UnifiedTokenVerifier(TokenVerifier):
"""
Unified token verifier supporting both multi-audience and token exchange modes.
Unified token verifier for multi-audience tokens (ADR-005).
Compliant with MCP security specification - no token pass-through.
This verifier:
1. Validates tokens using JWT verification with JWKS or introspection fallback
2. Enforces proper audience validation based on configured mode
2. Enforces MCP audience validation (per RFC 7519); Nextcloud independently
validates its own audience when receiving API calls
3. Caches successful validations to avoid repeated API calls
Mode Selection (via ENABLE_TOKEN_EXCHANGE setting):
- False/omit (default): Multi-audience mode - validates MCP audience only (per RFC 7519).
Nextcloud independently validates its own audience when receiving API calls.
- True: Exchange mode - requires MCP audience only, then exchanges for Nextcloud token
"""
def __init__(self, settings: Settings):
@@ -61,7 +57,6 @@ class UnifiedTokenVerifier(TokenVerifier):
settings: Application settings containing OAuth configuration
"""
self.settings = settings
self.mode = "multi-audience"
# Common components for all modes
self.http_client = nextcloud_httpx_client(timeout=10.0)
@@ -118,8 +113,7 @@ class UnifiedTokenVerifier(TokenVerifier):
)
logger.info(
"UnifiedTokenVerifier initialized in %s mode. MCP audience: %s or %s, Nextcloud resource URI: %s, Valid issuers: %s",
self.mode,
"UnifiedTokenVerifier initialized (multi-audience). MCP audience: %s or %s, Nextcloud resource URI: %s, Valid issuers: %s",
settings.oidc_client_id,
settings.nextcloud_mcp_server_url,
settings.nextcloud_resource_uri,
@@ -130,10 +124,9 @@ class UnifiedTokenVerifier(TokenVerifier):
"""
Verify token according to MCP TokenVerifier protocol.
Per RFC 7519, we validate only MCP audience. The mode determines what
happens AFTER verification in context_helper.py:
- Multi-audience mode: Use token directly (Nextcloud validates its own audience)
- Exchange mode: Exchange for Nextcloud-audience token via RFC 8693
Per RFC 7519, we validate only MCP audience. The token is then used
directly against Nextcloud (which validates its own audience) — see
context_helper.py.
Args:
token: Bearer token to verify
@@ -292,16 +285,10 @@ class UnifiedTokenVerifier(TokenVerifier):
record_oauth_token_validation(validation_method, "invalid")
return None
# Log based on mode for clarity
if self.mode == "multi-audience":
logger.info(
"MCP audience validated - token can be used directly "
"(Nextcloud will validate its own audience)"
)
else:
logger.info(
"MCP audience validated - token will be exchanged for Nextcloud access"
)
logger.info(
"MCP audience validated - token can be used directly "
"(Nextcloud will validate its own audience)"
)
return self._create_access_token(token, payload)
-6
View File
@@ -57,7 +57,6 @@ _DEFAULTS: dict[str, Any] = {
"enable_background_operations": False,
"vector_sync_enabled": False,
"enable_offline_access": False,
"enable_token_exchange": False,
# Token storage
"token_encryption_key": None,
# None = ephemeral per-process tempfile (see get_token_db_path()).
@@ -1268,7 +1267,6 @@ def _is_multi_user_mode() -> bool:
- Multi-user BasicAuth (MCP_DEPLOYMENT_MODE=multi_user_basic)
- Login Flow v2 / default OAuth (MCP_DEPLOYMENT_MODE=login_flow, or no
username/password and no explicit mode)
- OAuth Token Exchange (ENABLE_TOKEN_EXCHANGE=true)
Single-user mode is:
- Single-user BasicAuth (username and password both set)
@@ -1285,10 +1283,6 @@ def _is_multi_user_mode() -> bool:
if explicit_mode == "single_user_basic":
return False
# Token exchange implies OAuth multi-user
if _dynaconf.get("ENABLE_TOKEN_EXCHANGE", False):
return True
# If both username and password are set, it's single-user BasicAuth
has_username = bool(_dynaconf.get("NEXTCLOUD_USERNAME"))
has_password = bool(_dynaconf.get("NEXTCLOUD_PASSWORD"))