Skip to content

[C2S] Add DPoP (RFC 9449) support for OAuth token binding#2941

Draft
pfefferle wants to merge 96 commits intotrunkfrom
add/dpop-support
Draft

[C2S] Add DPoP (RFC 9449) support for OAuth token binding#2941
pfefferle wants to merge 96 commits intotrunkfrom
add/dpop-support

Conversation

@pfefferle
Copy link
Member

Fixes #

Proposed changes:

  • Add DPoP (Demonstrating Proof of Possession, RFC 9449) support for the C2S OAuth implementation. This cryptographically binds access tokens to a client's key pair, preventing token theft and replay attacks.
  • DPoP is fully opt-in: clients that include a DPoP proof header during token issuance get DPoP-bound tokens; clients that don't get regular Bearer tokens with no behavior change.
  • Self-contained implementation with no external JWT library — uses PHP's OpenSSL extension directly, compatible with PHP 7.2+.
  • Supports ES256 (ECDSA P-256) and RS256 signing algorithms.

Key changes:

  • New DPoP class (includes/oauth/class-dpop.php) — JWT decode, JWK thumbprint (RFC 7638), JWK-to-PEM conversion, DPoP proof validation with jti replay protection
  • Token class — stores dpop_jkt binding, returns token_type: DPoP, preserves binding through refresh, includes cnf.jkt in introspection
  • Server class — accepts Authorization: DPoP scheme, validates proofs on resource access, advertises dpop_signing_alg_values_supported in metadata, adds DPoP to CORS headers
  • OAuth_Controller — validates DPoP proofs at token endpoint for both auth code and refresh grants, verifies key consistency on refresh

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Run existing OAuth tests to verify no regressions: npm run env-test -- --group=oauth
  • Run DPoP-specific tests: npm run env-test -- --filter=DPoP
  • Verify server metadata includes dpop_signing_alg_values_supported: curl <site>/wp-json/activitypub/1.0/oauth/authorization-server-metadata | jq .dpop_signing_alg_values_supported
  • Existing Bearer token flows should work identically (backward compatible)

Changelog entry

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Added - for new features
  • Changed - for changes in existing functionality
  • Deprecated - for soon-to-be removed features
  • Removed - for now removed features
  • Fixed - for any bug fixes
  • Security - in case of vulnerabilities

Message

Add support for DPoP (RFC 9449) to protect OAuth tokens from theft and replay.

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)
- 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.
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.
The inbox GET endpoint now requires OAuth authentication (C2S).
E2E tests cannot easily test OAuth-protected endpoints without a full
OAuth flow. The functionality is covered by PHPUnit tests.
- Add verify_application_password() for WordPress core Application Passwords
- Add verify_authentication() that checks OAuth first, falls back to Application Passwords
- Update verify_owner() to support both OAuth tokens and WordPress users
- Simplify permission callbacks in controllers to use verify_authentication()
Enable browser-based C2S clients to interact with OAuth and ActivityPub
endpoints without requiring a proxy server.

Endpoints with CORS headers:
- OAuth: /token, /revoke, /introspect, /clients, /.well-known/oauth-authorization-server
- C2S: /proxy, /actors/{id}/outbox, /actors/{id}/inbox

The /oauth/authorize endpoint is excluded as it redirects to the login page.
Register /.well-known/oauth-authorization-server to comply with RFC 8414.
This enables OAuth clients to discover the server metadata at the
standard location.
Make PKCE optional and improve ActivityPub/client redirect handling:

- Authorization_Code::verify_pkce: Treat absence of a stored code_challenge as PKCE not used (skip verification); fail only when a challenge exists but no code_verifier is supplied.
- OAuth_Controller: Treat code_challenge as recommended (optional) and remove hard requirement/error for missing challenge/verifier to maintain compatibility with non-PKCE clients.
- Client metadata parsing: Accept ActivityPub actor types (Application, Person, Service, Group, Organization), map id/name (or preferredUsername) to client metadata, handle redirectURI/url, and set an is_actor flag for actor-based clients.
- Redirect validation: Allow RFC 8252 loopback redirect flexibility (ignore port differences for loopback hosts like 127.0.0.1, ::1, localhost) while preserving exact-match behavior for non-loopback URIs; added is_loopback_redirect_match helper.
- Server response handling: Exclude OAuth endpoints from the generic REST error format so OAuth endpoints keep RFC 6749 error responses.

These changes improve interoperability with ActivityPub actors, support permissive loopback redirect ports per RFC 8252, and maintain backward compatibility for clients that do not use PKCE.
Move from tests/rest/ to tests/includes/rest/ to match the convention
used by all other REST controller tests. Fix namespace accordingly.
Security fixes from code review:
- Bearer token auth takes priority over cookies
- Rate-limit client discovery and token endpoints
- Sanitize C2S content with wp_kses_post
- Capability checks in Create/Update handlers
- Ownership checks in Update/Undo handlers
- Scope introspection to same client
- Use wp_hash_password for client secrets
- Validate activity types, PKCE methods, redirect URIs
- Reflect CORS origin instead of wildcard
- Fix CORS exclusion matching authorization-server-metadata

Add changelog entry for the C2S feature branch.
Reverting the default to true since C2S clients rely on RFC 7591
dynamic registration to work out of the box.
Replace stale inline comment with a proper docblock explaining
the filter default and how to disable it.
Add an OAuth client registration form and token management section
to the user profile, following the WordPress core Application
Passwords UI pattern. Users can register new OAuth clients, view
credentials with copy-to-clipboard support, manage registered
applications, and revoke active tokens.
Rename the user settings section header to "OAuth Applications" and update the explanatory paragraph to clarify that users can register OAuth applications to connect third-party clients or revoke access for existing ones. This improves wording and clarity in the account settings UI (includes/wp-admin/class-user-settings-fields.php).
Split the Posts collection into two distinct classes:
- Remote_Posts: manages remote ActivityPub posts (ap_post type) received
  via federation (renamed from Posts)
- Posts: provides CRUD for local WordPress posts created via C2S outbox,
  with a prepare_content pipeline (wpautop, autolink, hashtags, blocks)

Also adds Blocks::convert_from_html() for converting HTML to WordPress
block markup, and updates all callers and tests accordingly.
Content now goes through prepare_content() which wraps HTML in
block markup. Use assertStringContainsString instead of assertEquals.
Mark phases 1-2 as implemented, add architecture notes for
collection split, content pipeline, and Connected Applications UI.
Remove the internal dev roadmap and add a short user-facing guide
for connecting ActivityPub clients to a WordPress site.
Broaden CORS headers to cover all ActivityPub REST namespace routes
(actors, followers, following, etc.), not just inbox/outbox. Also add
Accept to allowed headers for content negotiation.
Tests previously asserted actors and followers endpoints had no CORS
headers. Now that CORS covers the full AP namespace, update assertions
accordingly. Also verify Accept is in allowed headers.
Implement Demonstrating Proof of Possession to cryptographically bind
access tokens to a client's key pair, preventing token theft and replay.

DPoP is fully opt-in: clients that include a DPoP proof header during
token issuance get DPoP-bound tokens; clients that don't get regular
Bearer tokens with no behavior change.

- Add self-contained DPoP proof validator (ES256, RS256) with no
  external JWT library dependency
- Bind tokens to JWK thumbprint (RFC 7638) at issuance
- Validate DPoP proofs on resource access for bound tokens
- Preserve DPoP binding through token refresh
- Advertise dpop_signing_alg_values_supported in server metadata
- Add DPoP to CORS allowed headers
- Add 27 tests covering proof validation, key binding, and edge cases
@pfefferle pfefferle self-assigned this Feb 19, 2026
@pfefferle pfefferle requested a review from a team February 19, 2026 09:11
@pfefferle pfefferle changed the title Add DPoP (RFC 9449) support for OAuth token binding [C2S] Add DPoP (RFC 9449) support for OAuth token binding Feb 19, 2026
@pfefferle pfefferle added this to the ActivityPub API milestone Feb 19, 2026
Base automatically changed from add/c2s-support to trunk March 12, 2026 11:50
@github-actions github-actions bot added [Feature] CLI [Feature] Collections [Feature] WP Admin [Focus] Editor Changes to the ActivityPub experience in the block editor Docs labels Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants