fix: Simplify token verifier to be RFC 7519 compliant

Per RFC 7519 Section 4.1.3, resource servers should only validate their
own presence in the audience claim, not check for other resource servers.

Changes:
- UnifiedTokenVerifier now validates only MCP audience (not Nextcloud's)
- Nextcloud independently validates its own audience when receiving API calls
- This is NOT token passthrough (we validate tokens before use)
- This IS token reuse which is explicitly allowed by RFC 8707

Updates:
- Simplified _validate_multi_audience() to follow OAuth spec
- Updated docstrings and comments to clarify RFC 7519 compliance
- Fixed unit tests that expected dual-audience validation
- Updated ADR-005 to document the correct OAuth interpretation
- All tests pass: unit (65), smoke (5), OAuth integration

This makes the implementation simpler, more maintainable, and properly
aligned with OAuth 2.0 specifications while maintaining security.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-11-05 21:44:04 +01:00
co-authored by Claude
parent 877c4c91e0
commit 7d9ab5559c
6 changed files with 726 additions and 54 deletions
+14 -9
View File
@@ -94,7 +94,7 @@ class TestAudienceValidation:
assert verifier._validate_multi_audience(payload) is False
def test_validate_multi_audience_missing_nextcloud(self, base_settings):
"""Test multi-audience validation fails without Nextcloud audience."""
"""Test multi-audience validation succeeds with only MCP audience (RFC 7519 compliant)."""
verifier = UnifiedTokenVerifier(base_settings)
payload = {
"aud": ["test-client-id"], # Only MCP
@@ -102,10 +102,11 @@ class TestAudienceValidation:
"exp": int(time.time() + 3600),
}
assert verifier._validate_multi_audience(payload) is False
# Per RFC 7519, we only validate MCP audience. Nextcloud validates its own.
assert verifier._validate_multi_audience(payload) is True
def test_validate_multi_audience_string_audience(self, base_settings):
"""Test multi-audience validation with string audience (should still work)."""
"""Test multi-audience validation with string audience works (RFC 7519 compliant)."""
verifier = UnifiedTokenVerifier(base_settings)
payload = {
"aud": "test-client-id", # Single audience as string
@@ -113,8 +114,8 @@ class TestAudienceValidation:
"exp": int(time.time() + 3600),
}
# Should fail - needs both audiences
assert verifier._validate_multi_audience(payload) is False
# Should pass - we only validate MCP audience per RFC 7519
assert verifier._validate_multi_audience(payload) is True
def test_has_mcp_audience_with_client_id(self, exchange_settings):
"""Test MCP audience validation with client ID."""
@@ -266,14 +267,16 @@ class TestMultiAudienceVerification:
async def test_verify_multi_audience_fails_without_both_audiences(
self, base_settings
):
"""Test multi-audience verification fails without both audiences."""
"""Test multi-audience verification succeeds with only MCP audience (RFC 7519 compliant)."""
verifier = UnifiedTokenVerifier(base_settings)
# Mock introspection response with only one audience
# Mock introspection response with only MCP audience
introspection_response = {
"active": True,
"sub": "testuser",
"aud": ["test-client-id"], # Missing Nextcloud audience
"aud": [
"test-client-id"
], # Only MCP audience (Nextcloud validates its own)
"scope": "openid profile",
"exp": int(time.time() + 3600),
}
@@ -284,7 +287,9 @@ class TestMultiAudienceVerification:
opaque_token = "opaque-token-12345"
result = await verifier._verify_multi_audience_token(opaque_token)
assert result is None
# Should succeed with only MCP audience per RFC 7519
assert result is not None
assert result.resource == "testuser"
class TestExchangeModeVerification: