Skip to content

Conversation

@aaronjae22
Copy link
Collaborator

@aaronjae22 aaronjae22 commented Nov 20, 2025

This PR makes a review and update of the existing Actor Account Portability OAuth field and introduces a complete production-ready implementation of OAuth discovery and conditional migration properties specifications, with comprehensive test suite refactoring and configuration improvements.

Closes #230

Most of the infrastructure about this feature was already implemented but there were mismatches with LOLA specification that needed to be addressed.

accountPortabilityOAuth field

We were adding accountPortabilityOauth field only when authenticated with portability scope:

if auth_context and auth_context.get('has_portability_scope'):
    actor_data["accountPortabilityOauth"] = build_oauth_endpoint_url(request)
    # ... other LOLA fields

But the accountPortabilityOauth field must ALWAYS be present for OAuth endpoint discovery, regardless of authentication status.

Per LOLA spec: ActivityPub servers supporting this specification MUST provide the URL for their portability authorization endpoint in Actor objects, using the "accountPortabilityOauth" field.

Example of an Actor object with public response from LOLA specification:

  {
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://purl.archive.org/socialweb/blocked"
    ],
    "id": "https://oakfrost.example.com/brock",
    "type": "Person",
    "name": "Brock Oakfrost",
    "accountPortabilityOauth": "https://example.com/oauth2/porting-access-endpoint",
    "inbox": "https://oakfrost.example.com/brock/inbox"
  }
  • We now moved accountPortabilityOauth outside the authentication check to make it always present

Enhanced actor response with authorization token

Example of an Actor object response with account migration authorization token included in request

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://purl.archive.org/socialweb/blocked", # New - From LOLA examples
    "https://swicg.github.io/activitypub-data-portability/lola" # New - Custom Field to reference LOLA
   ],
  "id": "<https://oakfrost.example.com/brock>",
  "type": "Person",
  "name": "Brock Oakfrost",
  "accountPortabilityOauth": "<https://example.com/oauth2/porting-access-endpoint>",
  "inbox": "<https://oakfrost.example.com/brock/inbox>",
  "outbox": "<https://oakfrost.example.com/brock/outbox>",
  "following": "<https://oakfrost.example.com/brock/following>",
  "followers": "<https://oakfrost.example.com/brock/followers>",
  "liked": "<https://oakfrost.example.com/brock/liked>",
  "blocked": "<https://oakfrost.example.com/brock/blocked>",
  "migration": {
    "outbox": "<https://oakfrost.example.com/brock/migration/outbox>",
    "content": "<https://oakfrost.example.com/brock/migration/content>",
    "following": "<https://oakfrost.example.com/brock/migration/following>",
    "blocked": "<https://oakfrost.example.com/brock/migration/blocked>"
  }
}
  • Updated both Standard Collections and Authenticated-Only Collections to match LOLA specifications
  • Added blocked to context from LOLA specification
  • Added a custom field for referencing LOLA specification

migration URLs

In the LOLA spec example:

"migration": {
    "outbox": "<https://oakfrost.example.com/brock/migration/outbox>",
    "content": "<https://oakfrost.example.com/brock/migration/content>",
    "following": "<https://oakfrost.example.com/brock/migration/following>",
    "blocked": "<https://oakfrost.example.com/brock/migration/blocked>"
}

The URLs include /migration/ in the path but our implementation doesn't have those

"migration": {
    "outbox": f"{actor_id}/outbox",  # missing /migration/
    "content": f"{actor_id}/content",  # missing /migration/
    ...
}

But according to LOLA specification:

URL discovery allows the account migration “outbox” to be different from the normal outbox if the source server prefers to implement it at a different URL, but it can also be the same URL. There may be a number of differences between migration outbox and regular outbox, caused by permissions settings, filtering unnecessary items, filters explicitly chosen by the user, or other reasons we can't foresee now. That said, a simple implementation can probably be successful using the same URL for both.

LOLA spec allows flexibility here so I decided to keep the original implementation (without adding /migration) for now. It may be changed if necessary in the future.

OAuth Endpoint URL Builder

In def build_oauth_endpoint_url() in oauth_utils.py I updated the fallback for possible edge cases and add BASE_URL to the settings files since the fallback to https://example.com could possible cause issues and didn't look good.

def build_oauth_endpoint_url(request):

    if request:
        scheme = request.scheme
        host = request.get_host()
        return f"{scheme}://{host}/oauth/authorize/"
    else:
        # Fallback for edge cases (testing, background tasks, etc.)
        # In production, request should always be available for Actor endpoint responses
        logger.warning("build_oauth_endpoint_url called without request object - using BASE_URL from settings")
        from django.conf import settings
        return f"{settings.BASE_URL}/oauth/authorize/"

Current Behavior

# In build_oauth_endpoint_url(request)
if request:
    scheme = request.scheme  # 'http' or 'https'
    host = request.get_host()  # 'localhost:8000', 'ap-testbed.dtinit.org', etc.
    return f"{scheme}://{host}/oauth/authorize/"

Result:

  • Development: http://localhost:8000/oauth/authorize/
  • Staging: https://activitypub-testbed-stg-run-737003321709.us-central1.run.app
  • Production: https://ap-testbed.dtinit.org/oauth/authorize/

This works as expected. The URL is dynamically built from the actual request.

Adding BASE_URL to 6 settings files just allow us a proper fallback.

Actor JSON-LD Builder

  • Added the previously mentioned blocked context via BLOCKED constant and convert the JsonLDContext class to module-level constants for a more Pythonic approach (a goal in each of these reviews).
ACTIVITY_STREAM_CONTEXT = "https://www.w3.org/ns/activitystreams"
LOLA_CONTEXT = "https://swicg.github.io/activitypub-data-portability/lola.jsonld"
BLOCKED_CONTEXT = "https://purl.archive.org/socialweb/blocked"

# Return the extended context used specifically for Actor responses
def build_actor_context():
    return [
        ACTIVITY_STREAM_CONTEXT,
        BLOCKED_CONTEXT,
        LOLA_CONTEXT
    ]

Testing

  • Created a new test file called test_lola_actor.py that contains just the LOLA compliance tests related to this implementation (following pythonic approach) and frees up the size of the test_api.py file.
  • Use the correspondent factories.

Test implementation via Curl commands

1. Public Request (No Authentication)

This verifies that accountPortabilityOauth is always present but NO migration data:

curl -sS http://localhost:8000/api/actors/1/?format=json | jq '.'

Should be present: accountPortabilityOauth
Should not be present: migration, following, followers, liked, blocked, outbox

image

2. Authenticated Request (With Portability Token)

To test this we need a valid OAuth token with activitypub_account_portability scope.

Expected:accountPortabilityOauth, migration object, all collections (following, followers, liked, blocked, outbox)

curl -sS \
  -H "Authorization: Bearer test-lola-token-12345" \
  http://localhost:8000/api/actors/1/?format=json | jq '.'
image

3. Wrong Scope Test (Should Return Public Response)

curl -sS \
  -H "Authorization: Bearer WRONG_SCOPE_TOKEN" \
  http://localhost:8000/api/actors/1/?format=json | jq '.'

Expected: Same as public request (no migration data)

image

@aaronjae22 aaronjae22 self-assigned this Nov 20, 2025
@aaronjae22 aaronjae22 requested a review from lisad November 20, 2025 19:04
@aaronjae22 aaronjae22 force-pushed the feature/overview-actor-account-portability-oauth-field branch from 4d83e10 to f6acc0d Compare November 21, 2025 21:39
@aaronjae22 aaronjae22 changed the base branch from feature/overview-lola-rfc8414-discovery-endpoint to main November 21, 2025 21:40
@aaronjae22 aaronjae22 merged commit 59e9b0f into main Nov 21, 2025
3 checks passed
@aaronjae22 aaronjae22 deleted the feature/overview-actor-account-portability-oauth-field branch November 21, 2025 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Overview/Update] Actor Account Portability OAuth field

3 participants