[C2S] Add Client-to-Server ActivityPub API support#2851
Open
[C2S] Add Client-to-Server ActivityPub API support#2851
Conversation
Implements the SWICG ActivityPub API specification for C2S interactions: - OAuth 2.0 with PKCE authentication - POST to outbox for creating activities - GET inbox for reading received activities - Actor discovery with OAuth endpoints - Handlers for Create, Update, Delete, Follow, Undo activities New files: - includes/oauth/ - OAuth server, tokens, clients, auth codes, scopes - includes/rest/class-oauth-controller.php - OAuth endpoints Modified: - Outbox controller extended with POST support - Inbox controller extended with GET support - Handler classes extended with outbox handlers - Actor models include OAuth endpoints when C2S enabled - New activitypub_enable_c2s setting
Add C2S support for Like and Announce activities by hooking into the activitypub_handled_outbox_like and activitypub_handled_outbox_announce actions. These handlers fire corresponding sent actions that can be used to track when activities are sent via C2S.
Add comprehensive test coverage for the OAuth infrastructure: - Test_Scope: Scope parsing, validation, and string conversion - Test_Token: Token creation, validation, refresh, and revocation - Test_Client: Client registration, validation, and scope filtering - Test_Authorization_Code: PKCE flow, code exchange, and security checks
Remove type hint from get_items_permissions_check() to match the parent WP_REST_Controller class signature, which doesn't use type hints.
Remove type hint from create_item_permissions_check() to match the parent WP_REST_Controller class signature.
Remove type hint from create_item() to match the parent WP_REST_Controller class signature.
Constants cannot be covered by PHPUnit, only methods can.
Validate that submitted activities have actor/attributedTo fields matching the authenticated user. This prevents clients from submitting activities with mismatched actor data. Checks: - activity.actor must match authenticated user (if present) - object.attributedTo must match authenticated user (if present)
63225ba to
c498877
Compare
- Authorization codes now use WordPress transients (auto-expire after 10 min) - Tokens now use user meta instead of CPT (efficient per-user lookup) - Keep only Client CPT for persistent client registration - Add token introspection endpoint (RFC 7662) - Add revoke_for_client() method for cleanup when deleting clients - Add OAuth consent form template - Fix linting issues in Server class - Update tests for new error codes
- Rename handler methods to `incoming()` for inbox and `outgoing()` for outbox - Add deprecated proxy functions for backward compatibility (handle_*) - Update Create handler to support outbox POST with WordPress post creation - Add Dispatcher hook to fire outbox handlers after add_to_outbox() - Skip scheduler for already-federated posts to prevent duplicates - Remove C2S terminology from comments, use incoming/outgoing instead Handlers updated: Create, Update, Announce, Like, Undo, Follow, Delete
- Remove async scheduling from Post scheduler, call add_to_outbox directly - Create handler returns WP_Post instead of calling add_to_outbox - Add Outbox::get_by_object_id() to find outbox items by object ID and type - Controller handles WP_Post return from handlers and uses outbox_item directly
Update delete and update handlers to first resolve posts by permalink for C2S-created posts, falling back to GUID lookup for remote posts. Enhance OAuth server to respect previous auth errors and only process OAuth if C2S is enabled. Add type safety for user_id in REST controllers. Update template variable documentation and add PHPCS ignore comment in token class.
Contributor
|
One thing clients will need is a proxyURL endpoint, this allows the client to load Actor data from the inbox Activities This demo illustrates what I mean: https://social.coop/@django/115756317440812767 |
Pass the outbox item's ID instead of the object itself to the send_to_inboxes method in the test case. This aligns the test with the expected method signature.
- Add proxyUrl endpoint for C2S clients to fetch remote ActivityPub objects through the server's HTTP Signatures - Remove activitypub_enable_c2s option - C2S is now always enabled - Remove settings field for C2S toggle from advanced settings - Always include OAuth and C2S endpoints in actor profiles - Add security checks for proxy: HTTPS-only, block private networks - Use Remote_Actors::fetch_by_various() for efficient actor caching
- Add verify_oauth_read() and verify_oauth_write() methods to Server - Add verify_owner() to check token matches user_id parameter - Simplify permission checks in Inbox, Outbox, and Proxy controllers - Remove direct OAuth imports from controllers
- Create trait-verification.php with verify_signature, verify_oauth_read, verify_oauth_write, and verify_owner methods - Update controllers to use the trait instead of static Server methods - Maintain backwards compatibility by keeping static methods in Server class
- Update handler tests to use incoming() instead of deprecated handle_* methods - Add activitypub_oauth_check_permission filter for test mocking - Fix proxy controller tests to use rest_api_init for route registration - Update assertions to match actual return values (false vs null)
…oller Consolidates user inbox handling in the appropriate controller: - Actors_Inbox_Controller now handles user inbox GET (C2S) and POST (S2S) - Inbox_Controller now only handles shared inbox POST (S2S)
These methods are now provided by the Verification trait which controllers use directly. Removes 278 lines of duplicated code.
Remove get_create_item_args() method and inline the args directly in register_routes() for consistency with all other controllers. Also fix permission_callback to use the Verification trait method instead of the removed Server::verify_signature static method.
Test the three OAuth REST controllers (Authorization, Token, Clients) at the dispatch level covering route registration, parameter validation, authentication, and success flows.
Replace the monolithic OAuth_Controller with Authorization_Controller, Token_Controller, and Clients_Controller under Rest\OAuth namespace for better separation of concerns.
…, and test improvements - Add missing backslash prefixes for WordPress functions in OAuth and REST classes - Replace return null with return false in outbox handlers to prevent unhandled activities from falling through to raw outbox insertion - Return WP_Error from Update handler when Posts::update() fails instead of swallowing the error - Add explicit return values in Like and Announce handlers - Rename filter rest_activitypub_outbox_activity_types to activitypub_outbox_activity_types - Use strpos for CORS route matching instead of strict equality - Add per-user rate limiting (30/min) to proxy controller - Fix outbox pagination test to use a fresh user for empty collection assertions - Add @group annotations to all OAuth test classes
11 tasks
#2957) Co-authored-by: Matthias Pfefferle <pfefferle@users.noreply.github.com>
- Use wp_kses() instead of esc_html__() with printf() for HTML in OAuth consent template to prevent escaped tags from rendering as text. - Add backslash prefix to filter_var() calls in namespaced code. - Change @SInCE 1.3.0 to @SInCE unreleased on new outbox handlers hook. - Add Access-Control-Allow-Credentials header for browser-based C2S clients.
Extract IP detection into a helper with an `activitypub_client_ip` filter so sites behind reverse proxies can return the real client IP.
Cap active OAuth tokens at 50 per user. When exceeded, the oldest tokens are automatically revoked, preventing usermeta accumulation from repeated authentication.
Public clients can skip PKCE by default for backward compatibility, but site operators can require it via the `activitypub_oauth_require_pkce` filter, aligning with OAuth 2.1 which mandates PKCE for all public clients.
This reverts commit 099ee9e.
Replace hardcoded http://localhost:8889 with relative paths resolved against Playwright's baseURL config, and use the baseURL fixture for the webfinger resource parameter.
The uninstall hook was not removing OAuth data (ap_oauth_client posts and user meta tokens), leaving orphaned data in the database after plugin deletion.
Replace the serialized activitypub_oauth_token_users option with _activitypub_user_id post meta on ap_oauth_client posts, following the same pattern as _activitypub_following on ap_actor posts. This eliminates race conditions from concurrent read-modify-write on a shared option and allows direct client-to-user lookups.
mediaformat
reviewed
Feb 25, 2026
Add get_display_name() and get_link_url() to Client class so the template delegates display logic to the model. For DCR apps where client_id is a UUID, the link falls back to client_uri or the redirect URI origin instead of producing a broken link. Consolidate loose request variables into a single $authorize_params array to reduce template scope.
jeherve
reviewed
Feb 25, 2026
Member
jeherve
left a comment
There was a problem hiding this comment.
I won't claim to have thoroughly reviewed all of this PR 😅 But it looks good as it is, and I only have minor comments, below.
| } | ||
|
|
||
| // Authenticate via Bearer token for non-REST requests (e.g. permalink access). | ||
| if ( ! \is_user_logged_in() && ! \wp_is_serving_rest_request() ) { |
Member
There was a problem hiding this comment.
Do we need to limit this a bit more, maybe by checking for a role of at least author?
…new tab Add current_user_can checks to all OAuth AJAX handlers: manage_options for client management, read for token revocation. Open the client link on the consent form in a new tab to avoid breaking the OAuth flow.
…pport Add @see tags between Posts (C2S) and Remote_Posts (S2S) to clarify the collection split. Extend get_client_ip() to check common proxy headers (Cloudflare, X-Forwarded-For, etc.) before falling back to REMOTE_ADDR.
Prevents brute-force and abuse on the GET /oauth/authorize endpoint using the same transient-based pattern as the token endpoint (max 20 requests per minute per IP).
Activities like Arrive have no explicit `object` property and may provide `actor` as an object instead of a URI string. The previous code passed the raw actor array into `filter_var()` / `Webfinger::resolve()`, causing a PHP fatal. Use `object_to_uri()` to extract a string URI from the actor and validate the object ID before proceeding, returning a clean 400 error instead of a 500.
Update inline comment in includes/rest/class-outbox-controller.php to remove the 'C2S:' prefix and rephrase it as 'Default to public addressing if client omits recipients.' This is a non-functional change to improve clarity of the code comment.
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.
Fixes #1255
Proposed changes:
OAuth 2.0 Foundation
/.well-known/oauth-authorization-server.POST to Outbox
statuspost format; Articles as regular posts.prepare_content()pipeline:wpautop()→ link processing → hashtag processing → HTML-to-Gutenberg-blocks conversion.Collection\Postsfor CRUD operations.Inbox & Proxy
Architecture
Collection\PostsintoCollection\Posts(local CRUD for C2S) andCollection\Remote_Posts(federated remote posts from S2S).Handler\Outboxnamespace (separate from S2S inbox handlers).Blocks::convert_from_html()for converting raw HTML into Gutenberg block markup.Verificationtrait for centralized OAuth + scope authentication checks.Connected Applications UI
Other information:
Testing instructions:
oauthAuthorizationEndpoint,oauthTokenEndpoint).statusformat and block markup content.namefield and verify title and excerpt are set.Changelog entry
Changelog Entry Details
Significance
Type
Message
Support for ActivityPub Client-to-Server (C2S) protocol, allowing apps like federated clients to create, edit, and delete posts on your behalf.