docs(auth): document userinfo path in security model; drop dead guard; pin MCP asymmetry

Address claude-review round 8 on #919:
- Security-model docstring: note that opaque cross-client tokens authenticate
  via the userinfo liveness check (not JWKS/expiry) and bypass the client
  allowlist, with per-user authz as the gate.
- Remove the redundant `if not payload: return None` after the JWT/opaque
  branches (both already return None on failure) — replace with a comment.
- Add test_mcp_path_does_not_use_userinfo_for_opaque_token to pin that the
  userinfo fallback is management-path-only (MCP path still 401s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-17 20:40:05 +02:00
co-authored by Claude Opus 4.8
parent a53e6e7721
commit 7ef0e9d83b
2 changed files with 23 additions and 3 deletions
+15
View File
@@ -780,6 +780,21 @@ class TestUserinfoFallback:
assert result is not None
assert result.resource == "testuser"
async def test_mcp_path_does_not_use_userinfo_for_opaque_token(
self, userinfo_settings
):
"""The userinfo fallback applies only to the management API path, never
the MCP-audience path — an opaque token there is still rejected."""
verifier = UnifiedTokenVerifier(userinfo_settings)
userinfo_mock = AsyncMock(return_value={"sub": "testuser"})
with (
patch.object(verifier, "_introspect_token", AsyncMock(return_value=None)),
patch.object(verifier, "_validate_via_userinfo", userinfo_mock),
):
result = await verifier.verify_token("opaque-astrolabe-token")
assert result is None
userinfo_mock.assert_not_called()
async def test_opaque_rejected_when_no_validators_configured(self, base_settings):
"""With neither introspection nor userinfo configured, an opaque token is
rejected without recording a misleading userinfo-failure metric."""