PluginProbe
ActivityPub / 8.2.1
ActivityPub v8.2.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / rest / trait-verification.php

trait-verification.php in ActivityPub 8.2.1, at includes/rest/trait-verification.php

222 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Verification Trait file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Rest;
9
10 use Activitypub\Collection\Actors;
11 use Activitypub\OAuth\Scope;
12 use Activitypub\OAuth\Server as OAuth_Server;
13 use Activitypub\Signature;
14
15 use function Activitypub\object_to_uri;
16 use function Activitypub\use_authorized_fetch;
17
18 /**
19 * Verification Trait.
20 *
21 * Provides methods for verifying HTTP Signatures (S2S) and OAuth (C2S).
22 * Controllers can use this trait for permission callbacks.
23 */
24 trait Verification {
25 /**
26 * Verify HTTP Signature for server-to-server requests.
27 *
28 * Verifies the signature of POST, PUT, PATCH, and DELETE requests,
29 * as well as GET requests when authorized fetch is enabled.
30 * HEAD requests are bypassed by default so caches and link-checkers
31 * can probe public endpoints; callers that pass `$force_signature`
32 * (e.g. FEP-8fcf's `/followers/sync`) require signatures on HEAD too.
33 *
34 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
35 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
36 *
37 * @param \WP_REST_Request $request The request object.
38 * @param bool $force_signature Optional. When true, GET and HEAD requests also
39 * require a valid signature even with Authorized
40 * Fetch disabled. Use for endpoints that are
41 * peer-only (e.g. FEP-8fcf's `/followers/sync`).
42 * Default false.
43 * @return bool|\WP_Error True if authorized, WP_Error otherwise.
44 */
45 public function verify_signature( $request, $force_signature = false ) {
46 if ( 'HEAD' === $request->get_method() && ! $force_signature ) {
47 return true;
48 }
49
50 /**
51 * Filter to defer signature verification.
52 *
53 * Skip signature verification for debugging purposes or to reduce load for
54 * certain Activity-Types, like "Delete". Callers that want to preserve
55 * mandatory signing for endpoints passing `$force_signature = true`
56 * (e.g. FEP-8fcf's `/followers/sync`) should inspect the third argument
57 * and return `false` in that case.
58 *
59 * @param bool $defer Whether to defer signature verification.
60 * @param \WP_REST_Request $request The request used to generate the response.
61 * @param bool $force_signature Whether the caller has forced signature
62 * verification for this endpoint.
63 * @return bool Whether to defer signature verification.
64 */
65 $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request, $force_signature );
66
67 if ( $defer ) {
68 return true;
69 }
70
71 // POST-Requests always have to be signed, GET-Requests only require a signature in secure mode or when forced.
72 if ( 'GET' !== $request->get_method() || use_authorized_fetch() || $force_signature ) {
73 $verified_request = Signature::verify_http_signature( $request );
74 if ( \is_wp_error( $verified_request ) ) {
75 return new \WP_Error(
76 'activitypub_signature_verification',
77 $verified_request->get_error_message(),
78 array( 'status' => 401 )
79 );
80 }
81
82 // Verify the signing key's host matches the activity actor's host.
83 $key_id_check = $this->verify_key_id( $request );
84 if ( \is_wp_error( $key_id_check ) ) {
85 return $key_id_check;
86 }
87 }
88
89 return true;
90 }
91
92 /**
93 * Check that the signature keyId and activity actor share the same host.
94 *
95 * @since 8.1.0
96 *
97 * @param \WP_REST_Request $request The request object.
98 * @return true|\WP_Error True if valid, WP_Error on mismatch.
99 */
100 private function verify_key_id( $request ) {
101 $sig = $request->get_header( 'signature' );
102 if ( ! $sig || ! \preg_match( '/keyId="([^"]+)"/i', $sig, $m ) ) {
103 // RFC 9421 Signature-Input.
104 $sig = $request->get_header( 'signature-input' );
105 if ( ! $sig || ! \preg_match( '/keyid="([^"]+)"/i', $sig, $m ) ) {
106 return true;
107 }
108 }
109
110 $key_host = \strtolower( (string) \wp_parse_url( $m[1], \PHP_URL_HOST ) );
111 $json = $request->get_json_params();
112 $actor = isset( $json['actor'] ) ? object_to_uri( $json['actor'] ) : null;
113
114 if ( ! $actor || ! $key_host ) {
115 return true;
116 }
117
118 $actor_host = \strtolower( (string) \wp_parse_url( $actor, \PHP_URL_HOST ) );
119
120 if ( ! $actor_host || $key_host !== $actor_host ) {
121 return new \WP_Error(
122 'activitypub_key_actor_mismatch',
123 \__( 'Signing key and activity actor must be on the same host.', 'activitypub' ),
124 array( 'status' => 403 )
125 );
126 }
127
128 return true;
129 }
130
131 /**
132 * Verify user authentication via OAuth.
133 *
134 * Automatically determines the required scope based on the HTTP method:
135 * - GET, HEAD: read scope
136 * - POST, PUT, PATCH, DELETE: write scope
137 *
138 * If the request has a user_id parameter, also verifies that the
139 * authenticated user matches that actor.
140 *
141 * Application Passwords are not accepted directly on C2S endpoints.
142 *
143 * @param \WP_REST_Request $request The request object.
144 * @return bool|\WP_Error True if authorized, WP_Error otherwise.
145 */
146 public function verify_authentication( $request ) {
147 // Determine scope based on HTTP method.
148 $method = $request->get_method();
149 $read_methods = array( 'GET', 'HEAD' );
150 $scope = \in_array( $method, $read_methods, true ) ? Scope::READ : Scope::WRITE;
151
152 $result = OAuth_Server::check_oauth_permission( $request, $scope );
153 if ( true === $result ) {
154 return $this->maybe_verify_owner( $request );
155 }
156
157 return $result;
158 }
159
160 /**
161 * Verify owner if user_id parameter is present.
162 *
163 * @param \WP_REST_Request $request The request object.
164 * @return bool|\WP_Error True if authorized, WP_Error otherwise.
165 */
166 private function maybe_verify_owner( $request ) {
167 $user_id = $request->get_param( 'user_id' );
168
169 if ( null === $user_id ) {
170 return true;
171 }
172
173 return $this->verify_owner( $request );
174 }
175
176 /**
177 * Verify that the authenticated user matches the actor specified in the request.
178 *
179 * Checks that the user_id parameter matches the authenticated user.
180 * Works with both OAuth tokens and WordPress session auth (wp-login.php flow).
181 *
182 * @param \WP_REST_Request $request The request object.
183 * @return bool|\WP_Error True if the user matches, WP_Error otherwise.
184 */
185 public function verify_owner( $request ) {
186 $user_id = $request->get_param( 'user_id' );
187
188 // Validate the user exists.
189 $user = Actors::get_by_id( $user_id );
190 if ( \is_wp_error( $user ) ) {
191 return $user;
192 }
193
194 if ( \get_current_user_id() === (int) $user_id ) {
195 return true;
196 }
197
198 return new \WP_Error(
199 'activitypub_forbidden',
200 \__( 'You can only access your own resources.', 'activitypub' ),
201 array( 'status' => 403 )
202 );
203 }
204
205 /**
206 * Check if the social graph should be shown for this request.
207 *
208 * Returns true if the social graph setting allows public display,
209 * or if the request is authenticated by the resource owner.
210 *
211 * @since 8.1.0
212 *
213 * @param \WP_REST_Request $request The request object.
214 * @return bool True if the social graph should be shown.
215 */
216 protected function show_social_graph( $request ) {
217 $user_id = $request->get_param( 'user_id' );
218
219 return Actors::show_social_graph( $user_id ) || true === $this->verify_owner( $request );
220 }
221 }
222