feat: Initialize JWT-scoped tools
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Test JWT token structure and scope support.
|
||||
|
||||
This test obtains a JWT token via OAuth and examines its structure.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def decode_jwt_without_verification(token: str) -> dict:
|
||||
"""
|
||||
Decode JWT token without signature verification (for inspection only).
|
||||
|
||||
Returns:
|
||||
Dict with header and payload
|
||||
"""
|
||||
parts = token.split(".")
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid JWT format: expected 3 parts, got {len(parts)}")
|
||||
|
||||
# Decode header
|
||||
header = json.loads(
|
||||
base64.urlsafe_b64decode(parts[0] + "=" * (4 - len(parts[0]) % 4))
|
||||
)
|
||||
|
||||
# Decode payload
|
||||
payload = json.loads(
|
||||
base64.urlsafe_b64decode(parts[1] + "=" * (4 - len(parts[1]) % 4))
|
||||
)
|
||||
|
||||
return {
|
||||
"header": header,
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_jwt_token_structure_with_custom_client():
|
||||
"""
|
||||
Test that we can create a JWT-enabled OAuth client and examine the token structure.
|
||||
|
||||
This test manually configures a JWT client and obtains a token.
|
||||
"""
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
# This test requires manual setup of a JWT client
|
||||
# Skip if not configured
|
||||
client_id = os.getenv("NEXTCLOUD_JWT_CLIENT_ID")
|
||||
if not client_id:
|
||||
pytest.skip("NEXTCLOUD_JWT_CLIENT_ID not set - skipping JWT token test")
|
||||
|
||||
_client_secret = os.getenv("NEXTCLOUD_JWT_CLIENT_SECRET")
|
||||
nextcloud_host = os.getenv("NEXTCLOUD_HOST", "http://127.0.0.1:8080")
|
||||
|
||||
# Fetch discovery
|
||||
async with httpx.AsyncClient() as client:
|
||||
discovery_response = await client.get(
|
||||
f"{nextcloud_host}/.well-known/openid-configuration"
|
||||
)
|
||||
discovery_response.raise_for_status()
|
||||
discovery = discovery_response.json()
|
||||
|
||||
_token_endpoint = discovery["token_endpoint"]
|
||||
|
||||
# For this test, we'll use client credentials grant if supported
|
||||
# Otherwise, skip this test
|
||||
pytest.skip(
|
||||
"JWT token test requires OAuth flow - use manual testing script instead"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_opaque_token_vs_jwt_comparison():
|
||||
"""
|
||||
Compare opaque tokens vs JWT tokens to understand the differences.
|
||||
|
||||
This is a documentation test that explains the findings.
|
||||
"""
|
||||
# This test documents our findings about JWT vs opaque tokens
|
||||
# Based on manual testing with the test script
|
||||
|
||||
findings = {
|
||||
"oidc_app_capabilities": {
|
||||
"supports_jwt_tokens": True,
|
||||
"supports_opaque_tokens": True,
|
||||
"configuration_method": "per-client via token_type field",
|
||||
"jwt_standard": "RFC 9068 (OAuth 2.0 Access Token JWT Profile)",
|
||||
},
|
||||
"dynamic_registration": {
|
||||
"sets_allowed_scopes": False,
|
||||
"note": "Dynamic registration does NOT populate allowed_scopes from the scope parameter in registration request",
|
||||
"workaround": "Must use occ oidc:create with --allowed_scopes flag or manually update via web UI/API",
|
||||
},
|
||||
"jwt_token_structure": {
|
||||
"header": {
|
||||
"typ": "at+JWT", # RFC 9068 access token type
|
||||
"alg": "RS256", # Signature algorithm
|
||||
},
|
||||
"payload_claims": {
|
||||
"iss": "issuer URL",
|
||||
"sub": "user ID",
|
||||
"aud": "client ID",
|
||||
"exp": "expiration timestamp",
|
||||
"iat": "issued at timestamp",
|
||||
"scope": "space-separated scope string (THIS IS THE KEY!)",
|
||||
"client_id": "client identifier",
|
||||
"jti": "JWT ID",
|
||||
# Optional based on scopes:
|
||||
"roles": "if roles scope present",
|
||||
"groups": "if groups scope present",
|
||||
"email": "if email scope present",
|
||||
"name": "if profile scope present",
|
||||
},
|
||||
"scope_claim": {
|
||||
"format": "space-separated string",
|
||||
"example": "openid profile email nc:read nc:write",
|
||||
"extraction": "payload['scope'].split()",
|
||||
},
|
||||
},
|
||||
"scope_validation": {
|
||||
"oidc_app": {
|
||||
"validates": True,
|
||||
"method": "Intersects requested scopes with allowed_scopes per client",
|
||||
"location": "LoginRedirectorController.php:251-267",
|
||||
},
|
||||
"user_oidc_app": {
|
||||
"validates_scopes": False,
|
||||
"validates": ["token expiration", "issuer", "audience (optional)"],
|
||||
"limitation": "Does NOT extract or validate scopes from JWT",
|
||||
},
|
||||
},
|
||||
"token_size": {
|
||||
"opaque": "72 characters",
|
||||
"jwt": "~800-1200 characters (depends on claims)",
|
||||
"overhead": "JWT is 10-15x larger than opaque tokens",
|
||||
},
|
||||
"recommendation": {
|
||||
"for_mcp_server": "Use JWT tokens with self-validation",
|
||||
"reasoning": [
|
||||
"Can extract scopes directly from token payload",
|
||||
"No additional API call needed",
|
||||
"Standard approach (RFC 9068)",
|
||||
"Works with existing oidc app",
|
||||
],
|
||||
"alternative": "Implement introspection endpoint in oidc app (future work)",
|
||||
},
|
||||
}
|
||||
|
||||
# Print findings for documentation
|
||||
print("\n" + "=" * 80)
|
||||
print("JWT Token vs Opaque Token Findings")
|
||||
print("=" * 80)
|
||||
print(json.dumps(findings, indent=2))
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# This test always passes - it's for documentation
|
||||
assert True, "Findings documented"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_scope_presence_in_jwt():
|
||||
"""
|
||||
Verify that custom scopes (nc:read, nc:write) are present in JWT tokens.
|
||||
|
||||
NOTE: This test documents the expected behavior based on manual testing.
|
||||
Actual implementation will be tested in integration tests after JWT validation is implemented.
|
||||
"""
|
||||
expected_behavior = {
|
||||
"client_configuration": {
|
||||
"allowed_scopes": "openid profile email nc:read nc:write",
|
||||
"token_type": "jwt",
|
||||
},
|
||||
"authorization_request": {
|
||||
"scope": "openid profile email nc:read nc:write",
|
||||
},
|
||||
"token_response": {
|
||||
"access_token": "JWT with scope claim",
|
||||
},
|
||||
"jwt_payload": {
|
||||
"scope": "openid profile email nc:read nc:write", # All requested scopes present if in allowed_scopes
|
||||
},
|
||||
"scope_filtering": {
|
||||
"description": "oidc app filters requested scopes against allowed_scopes",
|
||||
"example": {
|
||||
"requested": "openid profile nc:read nc:write nc:admin",
|
||||
"allowed": "openid profile email nc:read nc:write",
|
||||
"granted": "openid profile nc:read nc:write", # nc:admin filtered out, email not requested
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("Expected JWT Scope Behavior")
|
||||
print("=" * 80)
|
||||
print(json.dumps(expected_behavior, indent=2))
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
assert True, "Expected behavior documented"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run with: uv run pytest tests/server/test_jwt_tokens.py -v
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
Reference in New Issue
Block a user