[C2S] Add Client-to-Server ActivityPub API support#2851
[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.
|
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.
| $redirect_host = $redirect_parts['host'] ?? ''; | ||
|
|
||
| // Must have same host. | ||
| if ( $allowed_host !== $redirect_host ) { |
There was a problem hiding this comment.
This allows omitting the hostname.
| * @param string $redirect_uri The requested redirect URI. | ||
| * @return bool True if they match under loopback rules. | ||
| */ | ||
| private static function is_loopback_redirect_match( $allowed_uri, $redirect_uri ) { |
There was a problem hiding this comment.
As written this function has several failure modes which could allow exploits.
You'd also allowing redirects to things with unknown query parameters here, which whilst may be technically allowed, could lead to security issues on localhost
| } | ||
|
|
||
| // Also revoke all tokens stored in user meta. | ||
| Token::revoke_all(); |
There was a problem hiding this comment.
Probably also revoke the AuthorizationCode's too, even though they should fail to exchange correctly.
| 'scopes_supported' => Scope::ALL, | ||
| 'response_types_supported' => array( 'code' ), | ||
| 'response_modes_supported' => array( 'query' ), | ||
| 'grant_types_supported' => array( 'authorization_code', 'refresh_token', 'password' ), |
There was a problem hiding this comment.
Password is deprecated in OAuth 2.1, and has been known to be insecure for a very long time
includes/oauth/class-server.php
Outdated
| 'response_types_supported' => array( 'code' ), | ||
| 'response_modes_supported' => array( 'query' ), | ||
| 'grant_types_supported' => array( 'authorization_code', 'refresh_token', 'password' ), | ||
| 'token_endpoint_auth_methods_supported' => array( 'none', 'client_secret_post' ), |
There was a problem hiding this comment.
There's also client_secret_basic but both of these should only be acceptable if the client has a client_secret (i.e., not valid for CIMDs)
- Remove same-origin redirect URI fallback (prevents open redirector) - Default scopes to read-only instead of read+write (fail-closed) - Default allowed_scopes to DEFAULT_SCOPES instead of ALL - Use client_uri for display links, not client_id (JSON endpoint) - Remove PKCE plain method (provides zero security) - Use 303 redirect instead of 302 per MDN recommendation - Guard AP actor-to-CIMD conversion against mixup attacks - Add more loopback address representations to HTTP check
Replace hardcoded localhost/loopback allowlist with a filter (activitypub_oauth_allow_http_redirect_uri) that defaults to false. Developers can enable http redirect URIs for local testing by hooking into the filter.
Extract client credentials from HTTP Basic Auth header (RFC 6749 Section 2.3.1) in addition to POST body parameters. Both methods are only used for confidential clients with a client_secret.
Allow http redirect URIs for loopback IP addresses (127.0.0.1, [::1]) as required by RFC 8252 for native app OAuth flows. Only IP-based loopback is accepted — DNS names like "localhost" are rejected since they could resolve to non-loopback addresses. Non-loopback http URIs remain blocked by default but can be enabled via the activitypub_oauth_allow_http_redirect_uri filter.
Extract loopback check into is_loopback_ip() helper that supports: - Full IPv4 loopback range 127.0.0.0/8 (RFC 1122 Section 3.2.1.3) - IPv6 loopback ::1 (RFC 4291 Section 2.5.3) - IPv4-mapped IPv6 loopback ::ffff:127.x.x.x (RFC 4291 Section 2.5.5.2) - Bracketed IPv6 as returned by parse_url DNS names like "localhost" remain excluded per RFC 8252 Section 8.3.
Use PHP's built-in FILTER_VALIDATE_IP with FILTER_FLAG_NO_RES_RANGE to detect loopback/reserved IPs instead of manual regex. This natively covers the full 127.0.0.0/8 range and ::1 without maintaining our own pattern.
On sites with persistent object cache and DB read-replicas, the token created by the password grant may not be immediately visible to validate() which uses a direct DB query. Cache the token hash to user_id mapping in the object cache so lookups succeed even before the replica catches up.
This reverts commit fad31f9.
authenticate_oauth runs on every REST request via rest_authentication_errors. Returning a WP_Error there prevents WordPress from dispatching the request at all — the permission callback never runs. This causes immediate 401 errors when token validation fails transiently (e.g. DB replication lag). Return null instead so the request is still dispatched and the endpoint's permission callback can enforce auth requirements.
…sion callback Remove the rest_authentication_errors filter hook entirely. Returning WP_Error from that filter causes WordPress to skip dispatch(), so the permission callback never runs. Instead, check_oauth_permission() now calls authenticate_oauth() lazily at permission-callback time. The authenticate_oauth() method is preserved for non-REST usage (e.g. Outbox permalink access).
…o permission callback" The rest_authentication_errors filter is needed for the success case: it calls wp_set_current_user() when a valid Bearer token is present. Without it, OAuth tokens never authenticate the user. The correct fix (from 987c1a2) is returning null on failure instead of WP_Error, which lets WordPress continue to dispatch(). This reverts commit d1ea5a7.
Endpoints using verify_signature (outbox GET, inbox POST) only accepted HTTP Signatures. C2S clients with valid OAuth Bearer tokens were rejected because the permission callback didn't check for OAuth. Now verify_signature delegates to verify_authentication when an OAuth token is present, allowing C2S clients to read/write via Bearer tokens. Also restores WP_Error return in authenticate_oauth for invalid tokens.
The OAuth check in verify_signature was harmful: it let OAuth tokens bypass HTTP signature verification on S2S POST endpoints (inbox). For GET requests without authorized fetch, verify_signature already returns true. OAuth authentication is handled by authenticate_oauth on the rest_authentication_errors filter which sets the current user. Also restores WP_Error return in authenticate_oauth for invalid Bearer tokens so they are properly rejected.
When a Bearer token is present, always attempt OAuth validation regardless of errors from earlier rest_authentication_errors filters. This prevents other plugins from blocking OAuth authentication.
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.