PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 5.4
MainWP Dashboard: Self-hosted WordPress Management for Agencies v5.4
6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 5.2.2 5.3 All 153 releases
mainwp / includes / rest-api / class-mainwp-rest-authentication.php

class-mainwp-rest-authentication.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 5.4, at includes/rest-api/class-mainwp-rest-authentication.php

879 lines 30.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST API Authentication
4 *
5 * @author Woocommercer author
6 * @package MainWP\Dashboard
7 * @since 5.1.1
8 */
9
10 use MainWP\Dashboard\MainWP_DB;
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * REST API authentication class.
16 */
17 class MainWP_REST_Authentication { //phpcs:ignore -- NOSONAR - maximumMethodThreshold.
18
19 //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
20
21 /**
22 * Authentication error.
23 *
24 * @var WP_Error
25 */
26 protected $error = null;
27
28 /**
29 * Logged in user data.
30 *
31 * @var stdClass
32 */
33 protected $user = null;
34
35 /**
36 * Current auth method.
37 *
38 * @var string
39 */
40 protected $auth_method = '';
41
42 /**
43 * Static variable to hold the single instance of the class.
44 *
45 * @static
46 *
47 * @var mixed Default null
48 */
49 public static $instance = null;
50
51 /**
52 * Get Instance
53 *
54 * Creates public static instance.
55 *
56 * @static
57 *
58 * @return class instance.
59 */
60 public static function get_instance() {
61 if ( null === static::$instance ) {
62 static::$instance = new self();
63 }
64 return static::$instance;
65 }
66
67 /**
68 * Init method.
69 *
70 * @static
71 *
72 * @return class instance.
73 */
74 public static function init() {
75 return static::get_instance();
76 }
77
78 /**
79 * Initialize authentication actions.
80 */
81 public function __construct() {
82 add_filter( 'determine_current_user', array( $this, 'authenticate' ), 15 );
83 add_filter( 'rest_authentication_errors', array( $this, 'authentication_fallback' ) );
84 add_filter( 'rest_authentication_errors', array( $this, 'check_authentication_error' ), 15 );
85 add_filter( 'rest_post_dispatch', array( $this, 'send_unauthorized_headers' ), 50 );
86 add_filter( 'rest_pre_dispatch', array( $this, 'check_user_permissions' ), 10, 3 );
87 }
88
89 /**
90 * Check if is request to our REST API.
91 *
92 * @return bool
93 */
94 protected function is_request_to_rest_api() {
95 if ( empty( $_SERVER['REQUEST_URI'] ) ) {
96 return false;
97 }
98
99 $rest_prefix = trailingslashit( rest_get_url_prefix() );
100 $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
101
102 // Check if the request is the API endpoints.
103 $mainwp_api = ( false !== strpos( $request_uri, $rest_prefix . 'mainwp/' ) );
104
105 // Allow third party plugins use our authentication methods.
106 $extension_api = ( false !== strpos( $request_uri, $rest_prefix . 'mainwp-' ) );
107
108 return apply_filters( 'mainwp_rest_is_request_to_rest_api', $mainwp_api || $extension_api );
109 }
110
111 /**
112 * Authenticate user.
113 *
114 * @param int|false $user_id User ID if one has been determined, false otherwise.
115 * @return int|false
116 */
117 public function authenticate( $user_id ) {
118 // Do not authenticate twice and check if is a request to our endpoint in the WP REST API.
119 if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) {
120 return $user_id;
121 }
122
123 if ( is_ssl() ) {
124 $user_id = $this->perform_basic_authentication();
125 }
126
127 if ( $user_id ) {
128 return $user_id;
129 }
130
131 $user_id = $this->perform_oauth_authentication();
132 if ( $user_id ) {
133 return $user_id;
134 }
135
136 if ( is_ssl() ) {
137 return $this->perform_basic_token_authentication();
138 }
139 }
140
141 /**
142 * Authenticate the user if authentication wasn't performed during the
143 * determine_current_user action.
144 *
145 * Necessary in cases where wp_get_current_user() is called before the plugin is loaded.
146 *
147 * @see https://github.com/woocommerce/woocommerce/issues/26847
148 *
149 * @param WP_Error|null|bool $error Error data.
150 * @return WP_Error|null|bool
151 */
152 public function authentication_fallback( $error ) {
153 if ( ! empty( $error ) ) {
154 // Another plugin has already declared a failure.
155 return $error;
156 }
157 if ( empty( $this->error ) && empty( $this->auth_method ) && empty( $this->user ) && 0 === get_current_user_id() ) {
158 // Authentication hasn't occurred during `determine_current_user`, so check auth.
159 $user_id = $this->authenticate( false );
160 if ( $user_id ) {
161 wp_set_current_user( $user_id );
162 return true;
163 }
164 }
165 return $error;
166 }
167
168 /**
169 * Check for authentication error.
170 *
171 * @param WP_Error|null|bool $error Error data.
172 * @return WP_Error|null|bool
173 */
174 public function check_authentication_error( $error ) {
175 // Pass through other errors.
176 if ( ! empty( $error ) ) {
177 return $error;
178 }
179
180 return $this->get_error();
181 }
182
183 /**
184 * Set authentication error.
185 *
186 * @param WP_Error $error Authentication error data.
187 */
188 protected function set_error( $error ) {
189 // Reset user.
190 $this->user = null;
191
192 $this->error = $error;
193 }
194
195 /**
196 * Get authentication error.
197 *
198 * @return WP_Error|null.
199 */
200 protected function get_error() {
201 return $this->error;
202 }
203
204 /**
205 * Basic Authentication.
206 *
207 * SSL-encrypted requests are not subject to sniffing or man-in-the-middle
208 * attacks, so the request can be authenticated by simply looking up the user
209 * associated with the given consumer key and confirming the consumer secret
210 * provided is valid.
211 *
212 * @return int|bool
213 */
214 private function perform_basic_authentication() {
215 $this->auth_method = 'basic_auth';
216 $consumer_key = '';
217 $consumer_secret = '';
218
219 //phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
220 // If the $_GET parameters are present, use those first.
221 if ( ! empty( $_GET['consumer_key'] ) && ! empty( $_GET['consumer_secret'] ) ) { // WPCS: CSRF ok.
222 $consumer_key = $_GET['consumer_key']; // WPCS: CSRF ok, sanitization ok.
223 $consumer_secret = $_GET['consumer_secret']; // WPCS: CSRF ok, sanitization ok.
224 }
225
226 // If the above is not present, we will do full basic auth.
227 if ( ! $consumer_key && ! empty( $_SERVER['PHP_AUTH_USER'] ) && ! empty( $_SERVER['PHP_AUTH_PW'] ) ) {
228 $consumer_key = $_SERVER['PHP_AUTH_USER']; // WPCS: CSRF ok, sanitization ok.
229 $consumer_secret = $_SERVER['PHP_AUTH_PW']; // WPCS: CSRF ok, sanitization ok.
230 }
231 //phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
232
233 // Stop if don't have any key.
234 if ( ! $consumer_key || ! $consumer_secret ) {
235 return false;
236 }
237
238 // Get user data.
239 $this->user = $this->get_user_data_by_consumer_key( $consumer_key );
240
241 if ( empty( $this->user ) ) {
242 return false;
243 }
244
245 // Validate user secret.
246 if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { // @codingStandardsIgnoreLine
247 $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer secret is invalid.', 'mainwp' ), array( 'status' => 401 ) ) );
248
249 return false;
250 }
251
252 return $this->user->user_id;
253 }
254
255 /**
256 * Parse the Authorization header into parameters.
257 *
258 * @since 3.0.0
259 *
260 * @param string $header Authorization header value (not including "Authorization: " prefix).
261 *
262 * @return array Map of parameter values.
263 */
264 public function parse_header( $header ) {
265 if ( 'OAuth ' !== substr( $header, 0, 6 ) ) {
266 return array();
267 }
268
269 // From OAuth PHP library, used under MIT license.
270 $params = array();
271 if ( preg_match_all( '/(oauth_[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches ) ) {
272 foreach ( $matches[1] as $i => $h ) {
273 $params[ $h ] = urldecode( empty( $matches[3][ $i ] ) ? $matches[4][ $i ] : $matches[3][ $i ] );
274 }
275 if ( isset( $params['realm'] ) ) {
276 unset( $params['realm'] );
277 }
278 }
279
280 return $params;
281 }
282
283 /**
284 * Parse the Authorization header into parameters.
285 *
286 * @since 3.0.0
287 *
288 * @param string $header Authorization header value (not including "Authorization: " prefix).
289 *
290 * @return array Map of parameter values.
291 */
292 public function parse_auth_header( $header ) {
293
294 if ( 'Bearer ' !== substr( $header, 0, 7 ) ) {
295 return array();
296 }
297
298 $header = trim( str_replace( 'Bearer ', '', $header ) );
299 $params = explode( '==', $header );
300 return array(
301 'oauth_mainwp_cs' => isset( $params[0] ) ? 'cs_' . $params[0] : '',
302 'oauth_mainwp_ck' => isset( $params[1] ) ? 'ck_' . $params[1] : '',
303 );
304 }
305
306 /**
307 * Get the authorization header.
308 *
309 * On certain systems and configurations, the Authorization header will be
310 * stripped out by the server or PHP. Typically this is then used to
311 * generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use
312 * `getallheaders` here to try and grab it out instead.
313 *
314 * @since 3.0.0
315 *
316 * @return string Authorization header if set.
317 */
318 public function get_authorization_header() {
319 if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
320 return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); //phpcs:ignore -- WPCS: sanitization ok.
321 }
322
323 if ( function_exists( 'getallheaders' ) ) {
324 $headers = getallheaders();
325 // Check for the authoization header case-insensitively.
326 foreach ( $headers as $key => $value ) {
327 if ( 'authorization' === strtolower( $key ) ) {
328 return $value;
329 }
330 }
331 }
332
333 return '';
334 }
335
336 /**
337 * Get oAuth parameters from $_GET, $_POST or request header.
338 *
339 * @since 3.0.0
340 *
341 * @return array|WP_Error
342 */
343 public function get_oauth_parameters() {
344 $params = array_merge( $_GET, $_POST ); //phpcs:ignore -- WPCS: CSRF ok.
345 $params = wp_unslash( $params );
346 $header = $this->get_authorization_header();
347 if ( ! empty( $header ) ) {
348 // Trim leading spaces.
349 $header = trim( $header );
350 $header_params = $this->parse_header( $header );
351
352 if ( ! empty( $header_params ) ) {
353 $params = array_merge( $params, $header_params );
354 }
355 }
356
357 $param_names = array(
358 'oauth_consumer_key',
359 'oauth_timestamp',
360 'oauth_nonce',
361 'oauth_signature',
362 'oauth_signature_method',
363 );
364
365 $errors = array();
366 $have_one = false;
367
368 // Check for required OAuth parameters.
369 foreach ( $param_names as $param_name ) {
370 if ( empty( $params[ $param_name ] ) ) {
371 $errors[] = $param_name;
372 } else {
373 $have_one = true;
374 }
375 }
376
377 // All keys are missing, so we're probably not even trying to use OAuth.
378 if ( ! $have_one ) {
379 return array();
380 }
381
382 // If we have at least one supplied piece of data, and we have an error,
383 // then it's a failed authentication.
384 if ( ! empty( $errors ) ) {
385 $message = sprintf(
386 /* translators: %s: amount of errors */
387 _n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'mainwp' ),
388 implode( ', ', $errors )
389 );
390
391 $this->set_error( new WP_Error( 'mainwp_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) );
392
393 return array();
394 }
395
396 return $params;
397 }
398
399
400 /**
401 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests.
402 *
403 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP.
404 *
405 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
406 *
407 * 1) There is no token associated with request/responses, only consumer keys/secrets are used.
408 *
409 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
410 * This is because there is no cross-OS function within PHP to get the raw Authorization header.
411 *
412 * @link http://tools.ietf.org/html/rfc5849 for the full spec.
413 *
414 * @return int|bool
415 */
416 private function perform_oauth_authentication() {
417 $this->auth_method = 'oauth1';
418
419 $params = $this->get_oauth_parameters();
420 if ( empty( $params ) ) {
421 return false;
422 }
423
424 // Fetch WP user by consumer key.
425 $this->user = $this->get_user_data_by_consumer_key( $params['oauth_consumer_key'] );
426
427 if ( empty( $this->user ) ) {
428 $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer key is invalid.', 'mainwp' ), array( 'status' => 401 ) ) );
429
430 return false;
431 }
432
433 // Perform OAuth validation.
434 $signature = $this->check_oauth_signature( $this->user, $params );
435 if ( is_wp_error( $signature ) ) {
436 $this->set_error( $signature );
437 return false;
438 }
439
440 $timestamp_and_nonce = $this->check_oauth_timestamp_and_nonce( $this->user, $params['oauth_timestamp'], $params['oauth_nonce'] );
441 if ( is_wp_error( $timestamp_and_nonce ) ) {
442 $this->set_error( $timestamp_and_nonce );
443 return false;
444 }
445
446 return $this->user->user_id;
447 }
448
449
450 /**
451 * Perform OAuth 1.0a "one-legged" (http://oauthbible.com/#oauth-10a-one-legged) authentication for non-SSL requests.
452 *
453 * This is required so API credentials cannot be sniffed or intercepted when making API requests over plain HTTP.
454 *
455 * This follows the spec for simple OAuth 1.0a authentication (RFC 5849) as closely as possible, with two exceptions:
456 *
457 * 1) There is no token associated with request/responses, only consumer keys/secrets are used.
458 *
459 * 2) The OAuth parameters are included as part of the request query string instead of part of the Authorization header,
460 * This is because there is no cross-OS function within PHP to get the raw Authorization header.
461 *
462 * @link http://tools.ietf.org/html/rfc5849 for the full spec.
463 *
464 * @return int|bool
465 */
466 private function perform_basic_token_authentication() {
467
468 $this->auth_method = 'oauth_token';
469
470 $params = $this->get_oauth_token_parameters();
471
472 if ( empty( $params ) ) {
473 return false;
474 }
475
476 $consumer_key = $params['oauth_mainwp_ck'];
477 $consumer_secret = $params['oauth_mainwp_cs'];
478
479 // Stop if don't have any key.
480 if ( ! $consumer_key || ! $consumer_secret ) {
481 return false;
482 }
483 // Get user data.
484 $this->user = $this->get_user_data_by_consumer_key( $consumer_key );
485 if ( empty( $this->user ) ) {
486 return false;
487 }
488 // Validate user secret.
489 if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { // @codingStandardsIgnoreLine
490 $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer secret is invalid.', 'mainwp' ), array( 'status' => 401 ) ) );
491
492 return false;
493 }
494 return $this->user->user_id;
495 }
496
497
498 /**
499 * Get oAuth parameters from $_GET, $_POST or request header.
500 *
501 * @since 3.0.0
502 *
503 * @return array|WP_Error
504 */
505 private function get_oauth_token_parameters() {
506 $this->auth_method = 'api_token';
507
508 $params = array_merge( $_GET, $_POST ); //phpcs:ignore -- WPCS: CSRF ok.
509 $params = wp_unslash( $params );
510 $header = $this->get_authorization_header();
511
512 if ( ! empty( $header ) ) {
513 // Trim leading spaces.
514 $header = trim( $header );
515 $header_params = $this->parse_auth_header( $header );
516
517 if ( ! empty( $header_params ) ) {
518 $params = array_merge( $params, $header_params );
519 }
520 }
521
522 $param_names = array(
523 'oauth_mainwp_ck',
524 'oauth_mainwp_cs',
525 );
526
527 $errors = array();
528 $have_one = false;
529
530 // Check for required OAuth parameters.
531 foreach ( $param_names as $param_name ) {
532 if ( empty( $params[ $param_name ] ) ) {
533 $errors[] = $param_name;
534 } else {
535 $have_one = true;
536 }
537 }
538
539 // All keys are missing, so we're probably not even trying to use OAuth.
540 if ( ! $have_one ) {
541 return array();
542 }
543
544 // If we have at least one supplied piece of data, and we have an error,
545 // then it's a failed authentication.
546 if ( ! empty( $errors ) ) {
547 $message = sprintf(
548 /* translators: %s: amount of errors */
549 _n( 'Missing OAuth parameter %s', 'Missing OAuth parameters %s', count( $errors ), 'mainwp' ),
550 implode( ', ', $errors )
551 );
552
553 $this->set_error( new WP_Error( 'mainwp_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) );
554
555 return array();
556 }
557
558 return $params;
559 }
560
561 /**
562 * Verify that the consumer-provided request signature matches our generated signature,
563 * this ensures the consumer has a valid key/secret.
564 *
565 * @param stdClass $user User data.
566 * @param array $params The request parameters.
567 * @return true|WP_Error
568 */
569 private function check_oauth_signature( $user, $params ) {
570 $http_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : ''; //phpcs:ignore -- WPCS: sanitization ok.
571 $request_path = isset( $_SERVER['REQUEST_URI'] ) ? wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : ''; //phpcs:ignore -- WPCS: sanitization ok.
572 $wp_base = get_home_url( null, '/', 'relative' );
573 if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) {
574 $request_path = substr( $request_path, strlen( $wp_base ) );
575 }
576 $base_request_uri = rawurlencode( get_home_url( null, $request_path, is_ssl() ? 'https' : 'http' ) );
577
578 // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature.
579 $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) );
580 unset( $params['oauth_signature'] );
581
582 // Sort parameters.
583 if ( ! uksort( $params, 'strcmp' ) ) {
584 return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - failed to sort parameters.', 'mainwp' ), array( 'status' => 401 ) );
585 }
586
587 // Normalize parameter key/values.
588 $params = $this->normalize_parameters( $params );
589 $query_string = implode( '%26', $this->join_with_equals_sign( $params ) ); // Join with ampersand.
590 $string_to_sign = $http_method . '&' . $base_request_uri . '&' . $query_string;
591
592 if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) {
593 return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'mainwp' ), array( 'status' => 401 ) );
594 }
595
596 $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) );
597 $secret = $user->consumer_secret . '&';
598 $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); //phpcs:ignore -- ok.
599
600 if ( ! hash_equals( $signature, $consumer_signature ) ) { // @codingStandardsIgnoreLine
601 return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - provided signature does not match.', 'mainwp' ), array( 'status' => 401 ) );
602 }
603 return true;
604 }
605
606 /**
607 * Creates an array of urlencoded strings out of each array key/value pairs.
608 *
609 * @param array $params Array of parameters to convert.
610 * @param array $query_params Array to extend.
611 * @param string $key Optional Array key to append.
612 * @return string Array of urlencoded strings.
613 */
614 private function join_with_equals_sign( $params, $query_params = array(), $key = '' ) {
615 foreach ( $params as $param_key => $param_value ) {
616 if ( $key ) {
617 $param_key = $key . '%5B' . $param_key . '%5D'; // Handle multi-dimensional array.
618 }
619
620 if ( is_array( $param_value ) ) {
621 $query_params = $this->join_with_equals_sign( $param_value, $query_params, $param_key );
622 } else {
623 $string = $param_key . '=' . $param_value; // Join with equals sign.
624 $query_params[] = mainwp_rest_urlencode_rfc3986( $string );
625 }
626 }
627 return $query_params;
628 }
629
630 /**
631 * Normalize each parameter by assuming each parameter may have already been
632 * encoded, so attempt to decode, and then re-encode according to RFC 3986.
633 *
634 * Note both the key and value is normalized so a filter param like:
635 *
636 * 'filter[period]' => 'week'
637 *
638 * is encoded to:
639 *
640 * 'filter%255Bperiod%255D' => 'week'
641 *
642 * This conforms to the OAuth 1.0a spec which indicates the entire query string
643 * should be URL encoded.
644 *
645 * @see rawurlencode()
646 * @param array $parameters Un-normalized parameters.
647 * @return array Normalized parameters.
648 */
649 private function normalize_parameters( $parameters ) {
650 $keys = mainwp_rest_urlencode_rfc3986( array_keys( $parameters ) );
651 $values = mainwp_rest_urlencode_rfc3986( array_values( $parameters ) );
652 return array_combine( $keys, $values );
653 }
654
655 /**
656 * Verify that the timestamp and nonce provided with the request are valid. This prevents replay attacks where
657 * an attacker could attempt to re-send an intercepted request at a later time.
658 *
659 * - A timestamp is valid if it is within 15 minutes of now.
660 * - A nonce is valid if it has not been used within the last 15 minutes.
661 *
662 * @param stdClass $user User data.
663 * @param int $timestamp The unix timestamp for when the request was made.
664 * @param string $nonce A unique (for the given user) 32 alphanumeric string, consumer-generated.
665 * @return bool|WP_Error
666 */
667 private function check_oauth_timestamp_and_nonce( $user, $timestamp, $nonce ) {
668 global $wpdb;
669
670 $valid_window = 15 * 60; // 15 minute window.
671
672 if ( ( $timestamp < time() - $valid_window ) || ( $timestamp > time() + $valid_window ) ) {
673 return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid timestamp.', 'mainwp' ), array( 'status' => 401 ) );
674 }
675
676 $used_nonces = maybe_unserialize( $user->nonces );
677
678 if ( empty( $used_nonces ) ) {
679 $used_nonces = array();
680 }
681
682 if ( in_array( $nonce, $used_nonces, true ) ) {
683 return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid nonce - nonce has already been used.', 'mainwp' ), array( 'status' => 401 ) );
684 }
685
686 $used_nonces[ $timestamp ] = $nonce;
687
688 // Remove expired nonces.
689 foreach ( $used_nonces as $nonce_timestamp => $nonce ) {
690 if ( $nonce_timestamp < ( time() - $valid_window ) ) {
691 unset( $used_nonces[ $nonce_timestamp ] );
692 }
693 }
694
695 $used_nonces = maybe_serialize( $used_nonces );
696
697 $wpdb->update(
698 MainWP_DB::instance()->get_table_name( 'api_keys' ),
699 array( 'nonces' => $used_nonces ),
700 array( 'key_id' => $user->key_id ),
701 array( '%s' ),
702 array( '%d' )
703 );
704
705 return true;
706 }
707
708 /**
709 * Return the user data for the given consumer_key.
710 *
711 * @param string $consumer_key Consumer key.
712 * @return mixed
713 */
714 private function get_user_data_by_consumer_key( $consumer_key ) {
715 global $wpdb;
716
717 $consumer_key = \mainwp_api_hash( sanitize_text_field( $consumer_key ) );
718 $user = $wpdb->get_row(
719 $wpdb->prepare(
720 '
721 SELECT * FROM ' .
722 MainWP_DB::instance()->get_table_name( 'api_keys' ) . '
723 WHERE consumer_key = %s
724 ',
725 $consumer_key
726 )
727 );
728
729 if ( empty( $user->enabled ) ) {
730 $this->set_error( new \WP_Error( 'mainwp_rest_authentication_disabled_key', __( 'The REST API Key are disabled.', 'mainwp' ), array( 'status' => 401 ) ) );
731 return false;
732 }
733 // phpcs:disable WordPress.Security.NonceVerification
734 $pass = ! empty( $_REQUEST['key_pass'] ) ? wp_unslash( $_REQUEST['key_pass'] ) : '';
735 // phpcs:enable WordPress.Security.NonceVerification
736 if ( 1 === (int) $user->key_type && $pass !== $user->key_pass ) {
737 $this->set_error( new \WP_Error( 'mainwp_rest_authentication_invalid_key_pass', __( 'The REST API passphrase is invalid.', 'mainwp' ), array( 'status' => 401 ) ) );
738 return false;
739 }
740 return $user;
741 }
742
743
744 /**
745 * Check that the API keys provided have the proper key-specific permissions to either read or write API resources.
746 *
747 * @param string $method Request method.
748 * @return bool|WP_Error
749 */
750 private function check_permissions( $method ) {
751 $permissions = $this->user->permissions;
752 $msg = '';
753 $flag = true;
754 switch ( $method ) {
755 case 'HEAD':
756 case 'GET':
757 if ( 'read' !== $permissions && 'read_write' !== $permissions ) {
758 $msg = __( 'The API key provided does not have read permissions.', 'mainwp' );
759 }
760 break;
761 case 'POST':
762 case 'PUT':
763 case 'PATCH':
764 case 'DELETE':
765 if ( 'write' !== $permissions && 'read_write' !== $permissions ) {
766 $msg = __( 'The API key provided does not have write permissions.', 'mainwp' );
767 }
768 break;
769 case 'OPTIONS':
770 $flag = true;
771 break;
772 default:
773 $msg = __( 'Unknown request method.', 'mainwp' );
774 }
775
776 if ( ! empty( $msg ) ) {
777 return new WP_Error( 'mainwp_rest_authentication_error', $msg, array( 'status' => 401 ) );
778
779 }
780
781 return $flag;
782 }
783
784 /**
785 * Updated API Key last access datetime.
786 */
787 private function update_last_access() {
788 global $wpdb;
789
790 /**
791 * This filter enables the exclusion of the most recent access time from being logged for REST API calls.
792 *
793 * @param bool $result Default value.
794 * @param int $key_id Key ID associated with REST API request.
795 * @param int $user_id User ID associated with REST API request.
796 *
797 * @since 5.1.1
798 */
799 if ( apply_filters( 'mainwp_disable_rest_api_access_log', false, $this->user->key_id, $this->user->user_id ) ) {
800 return;
801 }
802
803 $wpdb->update(
804 MainWP_DB::instance()->get_table_name( 'api_keys' ),
805 array( 'last_access' => current_time( 'mysql' ) ),
806 array( 'key_id' => $this->user->key_id ),
807 array( '%s' ),
808 array( '%d' )
809 );
810 }
811
812 /**
813 * If the consumer_key and consumer_secret $_GET parameters are NOT provided
814 * and the Basic auth headers are either not present or the consumer secret does not match the consumer
815 * key provided, then return the correct Basic headers and an error message.
816 *
817 * @param WP_REST_Response $response Current response being served.
818 * @return WP_REST_Response
819 */
820 public function send_unauthorized_headers( $response ) {
821 if ( is_wp_error( $this->get_error() ) && 'basic_auth' === $this->auth_method ) {
822 $auth_message = __( 'MainWP API. Use a consumer key in the username field and a consumer secret in the password field.', 'mainwp' );
823 $response->header( 'WWW-Authenticate', 'Basic realm="' . $auth_message . '"', true );
824 }
825
826 return $response;
827 }
828
829 /**
830 * Check for user permissions and register last access.
831 *
832 * @param mixed $result Response to replace the requested version with.
833 * @param WP_REST_Server $server Server instance.
834 * @param WP_REST_Request $request Request used to generate the response.
835 * @return mixed
836 */
837 public function check_user_permissions( $result, $server, $request ) {
838 unset( $server );
839 if ( $this->user ) {
840 // Check API Key permissions.
841 $allowed = $this->check_permissions( $request->get_method() );
842 if ( is_wp_error( $allowed ) ) {
843 return $allowed;
844 }
845
846 if ( ! defined( 'MAINWP_REST_API_DOING' ) ) {
847 define( 'MAINWP_REST_API_DOING', true );
848 }
849
850 // Register last access.
851 $this->update_last_access();
852 }
853
854 return $result;
855 }
856
857 /**
858 * Method get_rest_valid_user().
859 *
860 * @return mixed|object User api key object.
861 */
862 public function get_rest_valid_user() {
863 return $this->user;
864 }
865
866 /**
867 * Valid REST permissions.
868 *
869 * @param WP_REST_Request $request Request used to generate the response.
870 *
871 * @return mixed user rest data.
872 */
873 public function is_valid_permissions( $request ) {
874 return $this->check_permissions( $request->get_method() );
875 }
876 }
877
878 MainWP_REST_Authentication::init();
879