diff --git a/docs/lola-discovery.md b/docs/lola-discovery.md index 0618e95..cfffb86 100644 --- a/docs/lola-discovery.md +++ b/docs/lola-discovery.md @@ -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/" + } } ``` diff --git a/docs/oauth/phase-5-protected-resource-access.md b/docs/oauth/phase-5-protected-resource-access.md index 474a9bf..cdb7621 100644 --- a/docs/oauth/phase-5-protected-resource-access.md +++ b/docs/oauth/phase-5-protected-resource-access.md @@ -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" + } } ``` @@ -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 ``` @@ -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"], @@ -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/" + } } ``` @@ -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" + } } ``` diff --git a/testbed/core/json_ld_builders.py b/testbed/core/json_ld_builders.py index e376773..33bc0aa 100644 --- a/testbed/core/json_ld_builders.py +++ b/testbed/core/json_ld_builders.py @@ -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 @@ -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(), @@ -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): diff --git a/testbed/core/oauth/utils.py b/testbed/core/oauth/utils.py index ef91efe..24b3be5 100644 --- a/testbed/core/oauth/utils.py +++ b/testbed/core/oauth/utils.py @@ -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 diff --git a/testbed/core/templates/oauth_token_exchange.html b/testbed/core/templates/oauth_token_exchange.html index 0fd0ced..0b8cf65 100644 --- a/testbed/core/templates/oauth_token_exchange.html +++ b/testbed/core/templates/oauth_token_exchange.html @@ -135,7 +135,7 @@
accountPortabilityOauth, content, blocked, migrationendpoints.oauthMigrationEndpoint, migration.content, migration.blocked, migrationtotalItems count (includes private activities)