Skip to content

Commit eeec4e2

Browse files
authored
Merge pull request #229 from dtinit/feature/overview-lola-rfc8414-discovery-endpoint
feat: Update RFC8414 OAuth discovery endpoint for LOLA account portability Implementation
2 parents fb72bf7 + 712a6b1 commit eeec4e2

5 files changed

Lines changed: 135 additions & 76 deletions

File tree

testbed/core/tests/test_api.py

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -318,67 +318,6 @@ def test_content_type_headers_set_correctly(self):
318318
assert "accountPortabilityOauth" in data
319319

320320

321-
# LOLA Discovery Endpoint Tests
322-
323-
"""
324-
Tests for .well-known/oauth-authorization-server endpoint
325-
RFC8414-compliant OAuth Authorization Server Metadata with LOLA extensions
326-
"""
327-
class TestLOLADiscoveryEndpoint:
328-
329-
@pytest.mark.django_db
330-
def test_oauth_discovery_endpoint_returns_valid_metadata(self):
331-
"""Validate that discovery endpoint returns RFC8414-compliant OAuth metadata"""
332-
client = APIClient()
333-
334-
response = client.get('/.well-known/oauth-authorization-server')
335-
336-
assert response.status_code == status.HTTP_200_OK
337-
assert response['Content-Type'] == 'application/json'
338-
assert response['Access-Control-Allow-Origin'] == '*'
339-
340-
data = response.json()
341-
342-
# Check required OAuth metadata fields
343-
required_fields = [
344-
'issuer', 'authorization_endpoint', 'token_endpoint',
345-
'scopes_supported', 'response_types_supported', 'grant_types_supported'
346-
]
347-
348-
for field in required_fields:
349-
assert field in data, f"Missing required OAuth field: {field}"
350-
351-
@pytest.mark.django_db
352-
def test_discovery_includes_lola_scope_and_endpoint(self):
353-
"""Verify LOLA-specific parameters are included for account portability discovery"""
354-
client = APIClient()
355-
356-
response = client.get('/.well-known/oauth-authorization-server')
357-
data = response.json()
358-
359-
# LOLA scope should be supported
360-
assert 'activitypub_account_portability' in data['scopes_supported']
361-
362-
# LOLA endpoint parameter should be present
363-
assert 'activitypub_account_portability' in data
364-
assert data['activitypub_account_portability'].endswith('/oauth/authorize/')
365-
366-
@pytest.mark.django_db
367-
def test_discovery_endpoint_urls_are_absolute(self):
368-
"""Ensure all URLs in discovery response are absolute for federation compatibility"""
369-
client = APIClient()
370-
371-
response = client.get('/.well-known/oauth-authorization-server')
372-
data = response.json()
373-
374-
# All URL fields must be absolute for proper federation
375-
url_fields = ['issuer', 'authorization_endpoint', 'token_endpoint', 'activitypub_account_portability']
376-
377-
for field in url_fields:
378-
url = data[field]
379-
assert url.startswith('http'), f"{field} should be absolute URL: {url}"
380-
381-
382321
# LOLA Following Collection Tests
383322

384323
"""
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from rest_framework.test import APIClient
2+
from rest_framework import status
3+
4+
"""
5+
Tests for .well-known/oauth-authorization-server endpoint
6+
RFC8414-compliant OAuth Authorization Server Metadata with LOLA extensions
7+
"""
8+
9+
# Validate that discovery endpoint returns RFC8414-compliant OAuth metadata.
10+
def test_rfc8414_returns_valid_metadata():
11+
12+
client = APIClient()
13+
14+
response = client.get('/.well-known/oauth-authorization-server')
15+
16+
assert response.status_code == status.HTTP_200_OK
17+
assert response['Content-Type'] == 'application/json'
18+
assert response['Access-Control-Allow-Origin'] == '*'
19+
20+
data = response.json()
21+
22+
required_fields = [
23+
'issuer',
24+
'authorization_endpoint',
25+
'token_endpoint',
26+
'scopes_supported',
27+
'response_types_supported',
28+
'grant_types_supported'
29+
]
30+
31+
for field in required_fields:
32+
assert field in data, f"Missing required OAuth field: {field}"
33+
34+
# Verify LOLA-specific parameters are included for account portability discovery
35+
def test_rfc8414_includes_lola_parameters():
36+
37+
client = APIClient()
38+
39+
response = client.get('/.well-known/oauth-authorization-server')
40+
data = response.json()
41+
42+
# LOLA scope should be supported
43+
assert 'activitypub_account_portability' in data['scopes_supported'], \
44+
"LOLA scope 'activitypub_account_portability' must be in scopes_supported"
45+
46+
# LOLA endpoint parameter should be present (LOLA extension to RFC8414)
47+
assert 'activitypub_account_portability' in data, \
48+
"LOLA parameter 'activitypub_account_portability' must be present"
49+
assert data['activitypub_account_portability'].endswith('/oauth/authorize/'), \
50+
"LOLA portability endpoint should point to OAuth authorization endpoint"
51+
52+
53+
# Ensure all URLs in discovery response are absolute for federation compatibility
54+
def test_rfc8414_urls_are_absolute():
55+
56+
client = APIClient()
57+
58+
response = client.get('/.well-known/oauth-authorization-server')
59+
data = response.json()
60+
61+
# All URL fields must be absolute for proper federation
62+
url_fields = [
63+
'issuer',
64+
'authorization_endpoint',
65+
'token_endpoint',
66+
'activitypub_account_portability'
67+
]
68+
69+
for field in url_fields:
70+
url = data[field]
71+
assert url.startswith('http'), \
72+
f"{field} should be absolute URL starting with http/https: {url}"
73+
74+
75+
def test_rfc8414_authorization_code_flow_support():
76+
"""
77+
Verify that the authorization code flow is properly advertised.
78+
79+
LOLA uses OAuth 2.0 authorization code flow, so the metadata must
80+
indicate support for 'code' response type and 'authorization_code' grant type.
81+
"""
82+
client = APIClient()
83+
84+
response = client.get('/.well-known/oauth-authorization-server')
85+
data = response.json()
86+
87+
# Authorization code flow requirements
88+
assert 'code' in data['response_types_supported'], \
89+
"Must support 'code' response type for authorization code flow"
90+
assert 'authorization_code' in data['grant_types_supported'], \
91+
"Must support 'authorization_code' grant type"
92+
93+
94+
# Verify CORS headers are present to enable cross-origin discovery
95+
def test_rfc8414_cors_headers_for_federation():
96+
97+
client = APIClient()
98+
99+
response = client.get('/.well-known/oauth-authorization-server')
100+
101+
# CORS headers are essential for federation
102+
assert 'Access-Control-Allow-Origin' in response, \
103+
"CORS header 'Access-Control-Allow-Origin' must be present"
104+
assert response['Access-Control-Allow-Origin'] == '*', \
105+
"CORS should allow all origins for public discovery"
106+
107+
108+
# Verify that OAuth endpoint URLs match the actual django-oauth-toolkit routes.
109+
def test_rfc8414_oauth_endpoints_match():
110+
111+
client = APIClient()
112+
113+
response = client.get('/.well-known/oauth-authorization-server')
114+
data = response.json()
115+
116+
"""
117+
Check that OAuth endpoints use the correct paths
118+
This ensures consistency between the advertised endpoints and the actual OAuth implementation.
119+
"""
120+
assert '/oauth/authorize/' in data['authorization_endpoint'], \
121+
"Authorization endpoint should use /oauth/authorize/ path"
122+
assert '/oauth/token/' in data['token_endpoint'], \
123+
"Token endpoint should use /oauth/token/ path"
124+
125+
assert data['activitypub_account_portability'] == data['authorization_endpoint'], \
126+
"LOLA portability endpoint should point to the same authorization endpoint"

testbed/core/views.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -593,17 +593,17 @@ def blocked_collection(request, pk):
593593
return Response(collection_data)
594594

595595

596-
@api_view(['GET'])
597596
def oauth_authorization_server_metadata(request):
598597
"""
599598
RFC8414-compliant OAuth Authorization Server Metadata endpoint for LOLA discovery.
600599
601600
This endpoint enables automatic LOLA discovery by destination servers.
601+
602602
Per LOLA specification: "ActivityPub servers supporting this specification SHOULD
603603
include the URL of their portability authorization endpoint in their authorization
604604
server metadata document [RFC8414] using the activitypub_account_portability parameter."
605605
"""
606-
# Build the base URL dynamically from the request
606+
# Build base URL from request (HTTPS handled by SECURE_PROXY_SSL_HEADER in production)
607607
scheme = request.scheme
608608
host = request.get_host()
609609
base_url = f"{scheme}://{host}"
@@ -612,21 +612,15 @@ def oauth_authorization_server_metadata(request):
612612
"issuer": base_url,
613613
"authorization_endpoint": f"{base_url}{reverse('oauth2_provider:authorize')}",
614614
"token_endpoint": f"{base_url}{reverse('oauth2_provider:token')}",
615-
"scopes_supported": [
616-
"activitypub_account_portability"
617-
],
618-
"response_types_supported": [
619-
"code"
620-
],
621-
"grant_types_supported": [
622-
"authorization_code"
623-
],
615+
"scopes_supported": ["activitypub_account_portability"],
616+
"response_types_supported": ["code"],
617+
"grant_types_supported": ["authorization_code"],
624618
# LOLA-specific parameter for account portability endpoint discovery
625619
"activitypub_account_portability": f"{base_url}{reverse('oauth2_provider:authorize')}"
626620
}
627621

628622
response = JsonResponse(metadata)
629-
response['Access-Control-Allow-Origin'] = '*' # Enable federation
623+
response['Access-Control-Allow-Origin'] = '*' # CORS for federation
630624
return response
631625

632626

testbed/settings/production.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
ALLOWED_HOSTS = ["ap-testbed.dtinit.org", "www.ap-testbed.dtinit.org", "activitypub-testbed-prod-run-512458093489.us-central1.run.app"]
1111
SITE_URL = "https://ap-testbed.dtinit.org"
1212

13+
# Cloud Run uses X-Forwarded-Proto header for HTTPS detection
14+
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
15+
1316
# PostgreSQL for production
1417
DATABASES = {"default": env.db_url("DJ_DATABASE_CONN_STRING")}
1518

testbed/settings/staging.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
CSRF_TRUSTED_ORIGINS = ['https://' + url for url in ALLOWED_HOSTS]
88
GS_BUCKET_NAME = "activitypub-testbed-stg-storage"
99

10-
# Cloud Run uses X-Forwarded-Proto header for HTTPS detection
11-
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
12-
1310
# Update the STORAGES configuration with the staging bucket
1411
for key in STORAGES:
1512
STORAGES[key]["OPTIONS"]["bucket_name"] = GS_BUCKET_NAME

0 commit comments

Comments
 (0)