Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/lola-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,20 @@ curl -s https://localhost:8000/.well-known/oauth-authorization-server | \
While this implementation focuses on RFC8414 discovery, LOLA also supports Actor-based discovery. Both methods can coexist:

**RFC8414 Discovery**: `/.well-known/oauth-authorization-server`\
**Actor Discovery**: `accountPortabilityOauth` field in Actor objects
**Actor Discovery**: `endpoints.oauthMigrationEndpoint` field in Actor objects

Supporting servers MUST advertise their portability authorization endpoint in Actor objects under
`endpoints.oauthMigrationEndpoint`, in parallel with `endpoints.oauthAuthorizationEndpoint`:

```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Person",
"id": "https://source.example/actors/1",
"accountPortabilityOauth": "https://source.example/oauth/authorize/"
"endpoints": {
"oauthAuthorizationEndpoint": "https://source.example/oauth/authorize/",
"oauthMigrationEndpoint": "https://source.example/oauth/authorize/"
}
}
```

Expand Down
66 changes: 47 additions & 19 deletions docs/oauth/phase-5-protected-resource-access.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,22 @@ auth_context = {
### 4. Enhanced Data Features

#### **Actor Enhancements (LOLA Fields)**
When authenticated with portability scope, actor objects include:
The `endpoints` object is always public. When authenticated with portability scope,
actor objects additionally include the `migration` object plus the regular Actor collections:

```json
{
"accountPortabilityOauth": "https://source.example.com/oauth/authorize/",
"content": "https://source.example.com/api/actors/1/content",
"blocked": "https://source.example.com/api/actors/1/blocked",
"migration": "https://source.example.com/api/actors/1/outbox"
"endpoints": {
"oauthAuthorizationEndpoint": "https://source.example.com/oauth/authorize/",
"oauthMigrationEndpoint": "https://source.example.com/oauth/authorize/"
},
"liked": "https://source.example.com/api/actors/1/liked",
"migration": {
"outbox": "https://source.example.com/api/actors/1/migration/outbox",
"content": "https://source.example.com/api/actors/1/migration/content",
"following": "https://source.example.com/api/actors/1/migration/following",
"blocked": "https://source.example.com/api/actors/1/migration/blocked"
}
}
```

Expand Down Expand Up @@ -155,23 +163,31 @@ def authenticate(self, request):

```python
def build_actor_json_ld(actor, auth_context=None):
# Base ActivityPub Actor (always included)
# Base ActivityPub Actor (always included, public discovery surface).
# endpoints.oauthMigrationEndpoint MUST always be present.
actor_data = {
"@context": build_actor_context(),
"type": "Person",
"id": build_actor_id(actor.id),
"preferredUsername": actor.username,
# Standard ActivityPub collections
"outbox": f"{build_actor_id(actor.id)}/outbox",
# ...
"endpoints": {
"oauthAuthorizationEndpoint": build_oauth_endpoint_url(auth_context['request']),
"oauthMigrationEndpoint": build_oauth_endpoint_url(auth_context['request']),
},
}

# Add LOLA fields ONLY when authenticated with portability scope
if auth_context and auth_context.get('has_portability_scope'):
actor_data["accountPortabilityOauth"] = build_oauth_endpoint_url(auth_context['request'])
actor_data["content"] = f"{build_actor_id(actor.id)}/content"
actor_data["blocked"] = f"{build_actor_id(actor.id)}/blocked"
actor_data["migration"] = f"{build_actor_id(actor.id)}/outbox"
# Regular Actor collections (liked/followers are NOT migration collections)
actor_data["outbox"] = f"{build_actor_id(actor.id)}/outbox"
actor_data["liked"] = f"{build_actor_id(actor.id)}/liked"
# Migration feature discovery -> dedicated migration/... routes
actor_data["migration"] = {
"outbox": f"{build_actor_id(actor.id)}/migration/outbox",
"content": f"{build_actor_id(actor.id)}/migration/content",
"following": f"{build_actor_id(actor.id)}/migration/following",
"blocked": f"{build_actor_id(actor.id)}/migration/blocked",
}

return actor_data
```
Expand Down Expand Up @@ -208,6 +224,9 @@ def build_outbox_json_ld(outbox, auth_context=None):
## API Response Examples

### **Unauthenticated Actor Request**

The public response exposes the `endpoints` discovery object but omits the privacy-sensitive Actor collections and the `migration` object:

```json
{
"@context": ["https://www.w3.org/ns/activitystreams"],
Expand All @@ -216,9 +235,10 @@ def build_outbox_json_ld(outbox, auth_context=None):
"preferredUsername": "testuser",
"name": "testuser",
"inbox": "https://source.example.com/api/actors/1/inbox",
"outbox": "https://source.example.com/api/actors/1/outbox",
"followers": "https://source.example.com/api/actors/1/followers",
"following": "https://source.example.com/api/actors/1/following"
"endpoints": {
"oauthAuthorizationEndpoint": "https://source.example.com/oauth/authorize/",
"oauthMigrationEndpoint": "https://source.example.com/oauth/authorize/"
}
}
```

Expand All @@ -231,13 +251,21 @@ def build_outbox_json_ld(outbox, auth_context=None):
"preferredUsername": "testuser",
"name": "testuser",
"inbox": "https://source.example.com/api/actors/1/inbox",
"endpoints": {
"oauthAuthorizationEndpoint": "https://source.example.com/oauth/authorize/",
"oauthMigrationEndpoint": "https://source.example.com/oauth/authorize/"
},
"outbox": "https://source.example.com/api/actors/1/outbox",
"followers": "https://source.example.com/api/actors/1/followers",
"following": "https://source.example.com/api/actors/1/following",
"accountPortabilityOauth": "https://source.example.com/oauth/authorize/",
"content": "https://source.example.com/api/actors/1/content",
"liked": "https://source.example.com/api/actors/1/liked",
"blocked": "https://source.example.com/api/actors/1/blocked",
"migration": "https://source.example.com/api/actors/1/outbox"
"migration": {
"outbox": "https://source.example.com/api/actors/1/migration/outbox",
"content": "https://source.example.com/api/actors/1/migration/content",
"following": "https://source.example.com/api/actors/1/migration/following",
"blocked": "https://source.example.com/api/actors/1/migration/blocked"
}
}
```

Expand Down
38 changes: 23 additions & 15 deletions testbed/core/json_ld_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
# Build JSON-LD Actor with LOLA compliance.
def build_actor_json_ld(actor, auth_context=None):
"""
The accountPortabilityOauth field MUST always be present
for OAuth endpoint discovery (public visibility).

The migration.* properties are conditionally included only
when the request includes a valid portability token (scoped access).
Build an ActivityPub Actor object with revised-LOLA portability discovery.

- `endpoints.oauthMigrationEndpoint` MUST always be present
for OAuth endpoint discovery (public visibility).
- It is advertised in parallel with `endpoints.oauthAuthorizationEndpoint`.
Both point at `/oauth/authorize/` endpoint, which is also the URL advertised in the RFC8414 metadata.
- The `migration` object (outbox / content / following / blocked) is privacy-sensitive feature
discovery and is only included when the request carries a valid portability-scoped token.

Args:
actor: The Actor model instance
Expand All @@ -33,6 +36,10 @@ def build_actor_json_ld(actor, auth_context=None):
# Build actor URL
actor_id = build_actor_id(actor.id, request)

# The migration OAuth endpoint and the general OAuth authorization endpoint are the same URL,
# so both `endpoints.*` fields resolve to it. Computed once and reused.
oauth_authorize_url = build_oauth_endpoint_url(request)

# Base ActivityPub Actor (always included)
actor_data = {
"@context": build_actor_context(),
Expand All @@ -42,27 +49,28 @@ def build_actor_json_ld(actor, auth_context=None):
"name": actor.username,
"inbox": f"{actor_id}/inbox",
"previously": actor.previously or [],
"accountPortabilityOauth": build_oauth_endpoint_url(request)
"endpoints": {
"oauthAuthorizationEndpoint": oauth_authorize_url,
"oauthMigrationEndpoint": oauth_authorize_url,
},
}

# Privacy-sensitive fields ONLY with portability scope
if auth_context and auth_context.get('has_portability_scope'):
# Standard ActivityPub collections (privacy-sensitive)
actor_data["outbox"] = f"{actor_id}/outbox"
actor_data["following"] = f"{actor_id}/following"
actor_data["followers"] = f"{actor_id}/followers"
actor_data["liked"] = f"{actor_id}/liked"
actor_data["blocked"] = f"{actor_id}/blocked"

# LOLA migration endpoints (same URLs, scope-filtered responses)
# LOLA migration feature discovery
actor_data["migration"] = {
"outbox": f"{actor_id}/outbox",
"content": f"{actor_id}/content",
"following": f"{actor_id}/following",
"blocked": f"{actor_id}/blocked",
"liked": f"{actor_id}/liked"
"outbox": f"{actor_id}/migration/outbox",
"content": f"{actor_id}/migration/content",
"following": f"{actor_id}/migration/following",
"blocked": f"{actor_id}/migration/blocked",
}

return actor_data

def build_note_json_ld(note, auth_context=None):
Expand Down
8 changes: 4 additions & 4 deletions testbed/core/oauth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,10 @@ def build_oauth_endpoint_url(request):
The URL allows other ActivityPub services to discover where users can
authorize access for account migration.

Per LOLA specification: "ActivityPub servers supporting this specification
MUST provide the URL for their portability authorization endpoint in Actor
objects, using the 'accountPortabilityOauth' field."
Per LOLA specification: "Supporting servers MUST provide their portability authorization endpoint in Actor objects."
It is advertised under `endpoints.oauthMigrationEndpoint`." The Actor builder places this URL under
both `endpoints.oauthMigrationEndpoint` and the parallel `endpoints.oauthAuthorizationEndpoint`.

Args:
request: The HTTP request object containing scheme and host information

Expand Down
2 changes: 1 addition & 1 deletion testbed/core/templates/oauth_token_exchange.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ <h3>🧪 Test LOLA ActivityPub Endpoints</h3>
<div class="expected-results">
<h4>🔍 What You Should See:</h4>
<ul>
<li><strong>LOLA Links:</strong> Extra fields like <code>accountPortabilityOauth</code>, <code>content</code>, <code>blocked</code>, <code>migration</code></li>
<li><strong>LOLA Links:</strong> Extra fields like <code>endpoints.oauthMigrationEndpoint</code>, <code>migration.content</code>, <code>migration.blocked</code>, <code>migration</code></li>
<li><strong>LOLA Outbox:</strong> Higher <code>totalItems</code> count (includes private activities)</li>
<li><strong>Public Links:</strong> Basic ActivityPub data only, lower activity count</li>
</ul>
Expand Down
Loading
Loading