[PR #1] LOLA: implementing authorization redirect includes activitypub_actor - #255
Merged
aaronjae22 merged 4 commits intoJun 15, 2026
Merged
Conversation
aaronjae22
marked this pull request as ready for review
June 2, 2026 04:52
lisad
approved these changes
Jun 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #253
This PR implements LOLA §5.3:
### 5.3 Source server response If authorization is approved, the source server redirects the user back to the destination server with an authorization code with parameters: code - the authorization code state - the same random string passed by destination server activitypub_actor - the ActivityPub Actor ID associated with the one account that access is granted to. It is possible for the user to provide one Actor ID to the destination server, and then for the destination server to receive a different Actor ID in this response. The destination server MAY confirm this with the user but MUST use the Actor ID provided with the authorization code rather than the original one the user communicated.Before this PR the source emitted only
codeandstate. Now it will becomecode+state+activitypub_actor.LOLA §5.3 indicates that the source server's authorization-approval redirect carry the three previously mentioned query parameters.
codeandstateare produced bydjango-oauth-toolkit(DOT) as part of its standard OAuth 2.0 authorization-code.The implementation already has the important backend security property, it does actor-bound tokens but it does not yet surface that actor identity back in the authorization redirect. This feature does not "invent" an actor identity, it just exposing, earlier, the same actor we use later when issuing the token.
A few things worth to mention:
activitypub_actoris an absolute actor URL, consistent with the project's actor URLs.validators.py. What was missing was the earlier authorization redirect exposing the same actor identity to the destinationactivitypub_actor. It just simple logs it and return DOT's redirect unchanged so the flow doesn't silently emit a half-shaped LOLA redirect. In practice, every user has a source Actor via the post-save signal, this just guards the edge case.activitypub_account_portability, I leave the redirect alone. Non-LOLA OAuth flows are unaffected.path("oauth/authorize/", PortabilityAuthorizationView.as_view(), name="authorize")is registered before thepath("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")). Django dispatch is first-match-wins, so/oauth/authorize/resolves to the subclass while every other DOT endpoint (/oauth/token/,/oauth/revoke_token/, etc.) keeps resolving.form_valid(POST approval) andget()(which covers DOT'sskip_authorizationandapproval_prompt=autobranches). These are the places where DOT 3.0.1 callscreate_authorization_response()to build a success redirect, and they all are in the same class, so one helper covers all three with a single test per branch._append_actor_to_redirectkeep the parameter out of the wrong places: it only appends when the redirect's query carries code (so denial / error redirects likeerror=access_deniedare left alone), and on the GET path it only appends when the response is an actual 3xx; the 200 consent-form render passes through untouched._add_query_paramand_is_redirectare@staticmethodsince I just wanted to pack these methods under one class for organization.activitypub_bound_actor_idfeature. I had the idea of having a "shared attribute" mechanism which will allow stampingrequest.activitypub_bound_actor_id = actor.pkin the view and reading it back inActivityPubOAuth2Validator._save_bearer_token.PortabilityAuthorizationView._resolve_source_actor()andActivityPubOAuth2Validator._resolve_bound_actor()independently execute the sameActor.objects.get(user=user, role=ROLE_SOURCE)query, returning the same row becauseActor.clean()enforces one source Actor per user.save_authorization_codeand reusing it during token issuance. Today's one-source-actor-per-user invariant makes the deterministic-lookup mechanism sufficient.A bug that I need to take care of later on but worth mention now
First I decided to create a function
build_absolute_actor_urland not use the existingbuild_actor_id.The problem was that
build_absolute_actor_urlreturns thereverse()form, which includes the trailing slash, while the JSON-LDidfield everywhere else in the codebase emits the no-slashbuild-actor-idform.Since LOLA §5.3 requires the destination to compare/use activitypub_actor as the Actor ID, if the two strings aren't identical, a strict comparison on the destination side fails even though the same Actor is involved.
So I chose the no-slash form which is the canonical for this project so far. It's already what every Actor publishes as
id. Migrating everything to thereverse()slash form is a real refactor that touches JSON-LD output, existing tests, etc and is out of scope for this task.So we have two scenarios over here:
activitypub_actor= no-slash (delegate tobuild_actor_id)activitypub_actor= with-slash (reverse()form)Scenario A — send
/api/actors/1(no slash):/api/actors/1. No registered pattern matches because the only pattern is/api/actors/1/.APPEND_SLASH=True, which is the default. It tries appending a slash →/api/actors/1/. That matches a route, so the middleware returns an HTTP 301 redirect pointing the client at/api/actors/1/./api/actors/1/.The
APPEND_SLASHonly redirects for GET requests. For POST, etc, and unmatched no-slash URL would just return 404.