← All changes
|
includes/rest-api/class-mainwp-rest-authentication.php
+256
-45
5.2.1
→
6.2.1
View file →
| @@ -12,11 +12,41 @@ | ||
| 12 | 12 | defined( 'ABSPATH' ) || exit; |
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | 15 | * REST API authentication class. |
| 16 | + * | |
| 17 | + * This class handles authentication for MainWP's custom REST API endpoints. | |
| 18 | + * It supports Basic Authentication, OAuth 1.0a, and Bearer Token authentication. | |
| 19 | + * | |
| 20 | + * IMPORTANT: This authentication is ONLY applied to MainWP REST endpoints | |
| 21 | + * (namespaces starting with 'mainwp/' or 'mainwp-'). Other REST APIs, | |
| 22 | + * including the WordPress Abilities API (wp-abilities/v1/abilities/mainwp/...), | |
| 23 | + * use their own native WordPress authentication and MUST NOT be intercepted | |
| 24 | + * by MainWP's REST authentication. See is_request_to_rest_api() for details. | |
| 16 | 25 | */ |
| 17 | 26 | class MainWP_REST_Authentication { //phpcs:ignore -- NOSONAR - maximumMethodThreshold. |
| 18 | 27 | |
| 28 | + /** | |
| 29 | + * MainWP REST API namespace prefix. | |
| 30 | + * | |
| 31 | + * Used to identify requests that should be authenticated by MainWP's REST auth system. | |
| 32 | + * This prefix matches routes like /wp-json/mainwp/v2/sites but NOT routes like | |
| 33 | + * /wp-json/wp-abilities/v1/abilities/mainwp/list-sites-v1 (which use native WP auth). | |
| 34 | + * | |
| 35 | + * @var string | |
| 36 | + */ | |
| 37 | + const MAINWP_REST_NAMESPACE = 'mainwp/'; | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * MainWP extension REST API namespace prefix. | |
| 41 | + * | |
| 42 | + * Used to allow third-party MainWP extensions to use MainWP's REST auth. | |
| 43 | + * Matches routes like /wp-json/mainwp-extension/v1/... | |
| 44 | + * | |
| 45 | + * @var string | |
| 46 | + */ | |
| 47 | + const MAINWP_EXTENSION_REST_NAMESPACE = 'mainwp-'; | |
| 48 | + | |
| 19 | 49 | //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared |
| 20 | 50 | |
| 21 | 51 | /** |
| 22 | 52 | * Authentication error. |
| @@ -86,11 +116,29 @@ | ||
| 86 | 116 | add_filter( 'rest_pre_dispatch', array( $this, 'check_user_permissions' ), 10, 3 ); |
| 87 | 117 | } |
| 88 | 118 | |
| 89 | 119 | /** |
| 90 | - * Check if is request to our REST API. | |
| 120 | + * Check if this is a request to MainWP's REST API. | |
| 91 | 121 | * |
| 92 | - * @return bool | |
| 122 | + * This method determines whether the current request should use MainWP's | |
| 123 | + * custom REST API authentication (API keys, OAuth, Bearer tokens). | |
| 124 | + * | |
| 125 | + * Returns TRUE for: | |
| 126 | + * - /wp-json/mainwp/v2/sites (MainWP core REST API) | |
| 127 | + * - /wp-json/mainwp-extension/v1/foo (MainWP extension REST APIs) | |
| 128 | + * | |
| 129 | + * Returns FALSE for: | |
| 130 | + * - /wp-json/wp-abilities/v1/abilities/mainwp/list-sites-v1 (Abilities API) | |
| 131 | + * - /wp-json/wp/v2/posts (WordPress core REST API) | |
| 132 | + * - Any other REST endpoint not starting with 'mainwp/' or 'mainwp-' | |
| 133 | + * | |
| 134 | + * IMPORTANT: The Abilities API endpoints contain 'mainwp' in their path | |
| 135 | + * (as the ability provider namespace) but MUST use native WordPress | |
| 136 | + * authentication (cookie auth, application passwords, etc.) - NOT MainWP's | |
| 137 | + * API key system. This method's exact prefix matching ensures Abilities | |
| 138 | + * endpoints are excluded. | |
| 139 | + * | |
| 140 | + * @return bool True if request is to MainWP REST API, false otherwise. | |
| 93 | 141 | */ |
| 94 | 142 | protected function is_request_to_rest_api() { |
| 95 | 143 | if ( empty( $_SERVER['REQUEST_URI'] ) ) { |
| 96 | 144 | return false; |
| @@ -98,14 +146,25 @@ | ||
| 98 | 146 | |
| 99 | 147 | $rest_prefix = trailingslashit( rest_get_url_prefix() ); |
| 100 | 148 | $request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); |
| 101 | 149 | |
| 102 | - // Check if the request is the API endpoints. | |
| 103 | - $mainwp_api = ( false !== strpos( $request_uri, $rest_prefix . 'mainwp/' ) ); | |
| 150 | + // Check if the request is to the MainWP API namespace. | |
| 151 | + // Use strpos with exact prefix to avoid false positives from other APIs | |
| 152 | + // that may have 'mainwp/' in their URL path (e.g., wp-abilities/v1/abilities/mainwp/...). | |
| 153 | + $mainwp_api = ( false !== strpos( $request_uri, '/' . $rest_prefix . self::MAINWP_REST_NAMESPACE ) ); | |
| 104 | 154 | |
| 105 | 155 | // Allow third party plugins use our authentication methods. |
| 106 | - $extension_api = ( false !== strpos( $request_uri, $rest_prefix . 'mainwp-' ) ); | |
| 156 | + $extension_api = ( false !== strpos( $request_uri, '/' . $rest_prefix . self::MAINWP_EXTENSION_REST_NAMESPACE ) ); | |
| 107 | 157 | |
| 158 | + /** | |
| 159 | + * Filter whether a request is considered to be to the MainWP REST API. | |
| 160 | + * | |
| 161 | + * IMPORTANT: Do NOT add Abilities API endpoints (wp-abilities/v1/abilities/mainwp/...) | |
| 162 | + * to this filter. Those endpoints use native WordPress authentication and should | |
| 163 | + * NOT be processed by MainWP's REST authentication system. | |
| 164 | + * | |
| 165 | + * @param bool $is_mainwp_api True if this is a MainWP REST API request. | |
| 166 | + */ | |
| 108 | 167 | return apply_filters( 'mainwp_rest_is_request_to_rest_api', $mainwp_api || $extension_api ); |
| 109 | 168 | } |
| 110 | 169 | |
| 111 | 170 | /** |
| @@ -119,9 +178,9 @@ | ||
| 119 | 178 | if ( ! empty( $user_id ) || ! $this->is_request_to_rest_api() ) { |
| 120 | 179 | return $user_id; |
| 121 | 180 | } |
| 122 | 181 | |
| 123 | - if ( is_ssl() ) { | |
| 182 | + if ( $this->is_ssl_request() ) { | |
| 124 | 183 | $user_id = $this->perform_basic_authentication(); |
| 125 | 184 | } |
| 126 | 185 | |
| 127 | 186 | if ( $user_id ) { |
| @@ -132,14 +191,52 @@ | ||
| 132 | 191 | if ( $user_id ) { |
| 133 | 192 | return $user_id; |
| 134 | 193 | } |
| 135 | 194 | |
| 136 | - if ( is_ssl() ) { | |
| 195 | + if ( ! $this->is_ssl_request() ) { | |
| 196 | + $this->set_error( new \WP_Error( 'mainwp_rest_authentication_bad_request', __( 'Requests to the MainWP REST API must be over HTTPS.', 'mainwp' ), array( 'status' => 400 ) ) ); | |
| 197 | + return false; | |
| 198 | + } else { | |
| 137 | 199 | return $this->perform_basic_token_authentication(); |
| 138 | 200 | } |
| 139 | 201 | } |
| 140 | 202 | |
| 141 | 203 | /** |
| 204 | + * Method is_ssl_request(). | |
| 205 | + * | |
| 206 | + * Check is https request. | |
| 207 | + * | |
| 208 | + * @return bool | |
| 209 | + */ | |
| 210 | + private function is_ssl_request() { | |
| 211 | + if ( is_ssl() || wp_is_using_https() || $this->is_local_request() ) { | |
| 212 | + return true; | |
| 213 | + } | |
| 214 | + return false; | |
| 215 | + } | |
| 216 | + | |
| 217 | + /** | |
| 218 | + * Method is_local_request(). | |
| 219 | + * | |
| 220 | + * @return bool | |
| 221 | + */ | |
| 222 | + private function is_local_request() { | |
| 223 | + | |
| 224 | + if ( ! isset( $_SERVER['REMOTE_ADDR'] ) ) { | |
| 225 | + return false; | |
| 226 | + } | |
| 227 | + | |
| 228 | + $ip = $_SERVER['REMOTE_ADDR']; //phpcs:ignore -- ok. | |
| 229 | + | |
| 230 | + // Always allow localhost. | |
| 231 | + if ( '127.0.0.1' === $ip || '::1' === $ip ) { | |
| 232 | + return true; | |
| 233 | + } | |
| 234 | + return false; | |
| 235 | + } | |
| 236 | + | |
| 237 | + | |
| 238 | + /** | |
| 142 | 239 | * Authenticate the user if authentication wasn't performed during the |
| 143 | 240 | * determine_current_user action. |
| 144 | 241 | * |
| 145 | 242 | * Necessary in cases where wp_get_current_user() is called before the plugin is loaded. |
| @@ -176,8 +273,15 @@ | ||
| 176 | 273 | if ( ! empty( $error ) ) { |
| 177 | 274 | return $error; |
| 178 | 275 | } |
| 179 | 276 | |
| 277 | + // Only return MainWP auth errors for MainWP API endpoints. | |
| 278 | + // Other REST endpoints (e.g., wp-abilities, core WordPress) should use | |
| 279 | + // their own authentication mechanisms without MainWP interference. | |
| 280 | + if ( ! $this->is_request_to_rest_api() ) { | |
| 281 | + return $error; | |
| 282 | + } | |
| 283 | + | |
| 180 | 284 | return $this->get_error(); |
| 181 | 285 | } |
| 182 | 286 | |
| 183 | 287 | /** |
| @@ -201,8 +305,47 @@ | ||
| 201 | 305 | return $this->error; |
| 202 | 306 | } |
| 203 | 307 | |
| 204 | 308 | /** |
| 309 | + * Detect whether a stored consumer_secret value is still in legacy plaintext format. | |
| 310 | + * | |
| 311 | + * Legacy v2 keys were stored as `cs_` followed by hex digits (production uses | |
| 312 | + * 40 hex chars; some older fixtures use 32). New keys created after MWP-1540 | |
| 313 | + * are hashed via wp_hash_password and always begin with `$` (`$P$`, `$wp$`, | |
| 314 | + * `$argon2id$`, etc.). The presence of the `cs_` + hex prefix is sufficient | |
| 315 | + * to distinguish a plaintext token from any password-hash output. | |
| 316 | + * | |
| 317 | + * @todo MWP-1540 sunset (~v6.3): once the migration has shipped for a couple | |
| 318 | + * of minor versions and release notes have given customers time to | |
| 319 | + * rotate their keys, this helper and the legacy branch inside | |
| 320 | + * verify_consumer_secret() can both be removed. | |
| 321 | + * | |
| 322 | + * @param string $value Stored consumer_secret value. | |
| 323 | + * @return bool True when the value is legacy plaintext. | |
| 324 | + */ | |
| 325 | + private function is_legacy_plaintext_secret( $value ) { | |
| 326 | + return is_string( $value ) && (bool) preg_match( '/^cs_[a-f0-9]+$/', $value ); | |
| 327 | + } | |
| 328 | + | |
| 329 | + /** | |
| 330 | + * Verify a submitted consumer_secret against the stored value. | |
| 331 | + * | |
| 332 | + * Handles both legacy plaintext (constant-time compare via hash_equals) and | |
| 333 | + * hashed (wp_check_password) formats. Does NOT auto-rehash legacy rows; | |
| 334 | + * existing plaintext keys stay plaintext until the admin reissues them. | |
| 335 | + * | |
| 336 | + * @param string $stored Stored consumer_secret from the database. | |
| 337 | + * @param string $submitted Consumer_secret submitted in the request. | |
| 338 | + * @return bool True if the submitted secret matches the stored value. | |
| 339 | + */ | |
| 340 | + private function verify_consumer_secret( $stored, $submitted ) { | |
| 341 | + if ( $this->is_legacy_plaintext_secret( $stored ) ) { | |
| 342 | + return hash_equals( $stored, $submitted ); // @codingStandardsIgnoreLine | |
| 343 | + } | |
| 344 | + return wp_check_password( $submitted, $stored ); | |
| 345 | + } | |
| 346 | + | |
| 347 | + /** | |
| 205 | 348 | * Basic Authentication. |
| 206 | 349 | * |
| 207 | 350 | * SSL-encrypted requests are not subject to sniffing or man-in-the-middle |
| 208 | 351 | * attacks, so the request can be authenticated by simply looking up the user |
| @@ -241,10 +384,10 @@ | ||
| 241 | 384 | if ( empty( $this->user ) ) { |
| 242 | 385 | return false; |
| 243 | 386 | } |
| 244 | 387 | |
| 245 | - // Validate user secret. | |
| 246 | - if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { // @codingStandardsIgnoreLine | |
| 388 | + // Validate user secret. Handles both legacy plaintext rows and hashed rows. | |
| 389 | + if ( ! $this->verify_consumer_secret( $this->user->consumer_secret, $consumer_secret ) ) { | |
| 247 | 390 | $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer secret is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); |
| 248 | 391 | |
| 249 | 392 | return false; |
| 250 | 393 | } |
| @@ -381,14 +524,9 @@ | ||
| 381 | 524 | |
| 382 | 525 | // If we have at least one supplied piece of data, and we have an error, |
| 383 | 526 | // then it's a failed authentication. |
| 384 | 527 | 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 | - | |
| 528 | + $message = __( 'Invalid OAuth parameter.', 'mainwp' ); | |
| 391 | 529 | $this->set_error( new WP_Error( 'mainwp_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) ); |
| 392 | 530 | |
| 393 | 531 | return array(); |
| 394 | 532 | } |
| @@ -484,10 +622,10 @@ | ||
| 484 | 622 | $this->user = $this->get_user_data_by_consumer_key( $consumer_key ); |
| 485 | 623 | if ( empty( $this->user ) ) { |
| 486 | 624 | return false; |
| 487 | 625 | } |
| 488 | - // Validate user secret. | |
| 489 | - if ( ! hash_equals( $this->user->consumer_secret, $consumer_secret ) ) { // @codingStandardsIgnoreLine | |
| 626 | + // Validate user secret. Handles both legacy plaintext rows and hashed rows. | |
| 627 | + if ( ! $this->verify_consumer_secret( $this->user->consumer_secret, $consumer_secret ) ) { | |
| 490 | 628 | $this->set_error( new WP_Error( 'mainwp_rest_authentication_error', __( 'Consumer secret is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); |
| 491 | 629 | |
| 492 | 630 | return false; |
| 493 | 631 | } |
| @@ -543,16 +681,10 @@ | ||
| 543 | 681 | |
| 544 | 682 | // If we have at least one supplied piece of data, and we have an error, |
| 545 | 683 | // then it's a failed authentication. |
| 546 | 684 | 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 | - | |
| 685 | + $message = __( 'Invalid OAuth parameter.', 'mainwp' ); | |
| 553 | 686 | $this->set_error( new WP_Error( 'mainwp_rest_authentication_missing_parameter', $message, array( 'status' => 401 ) ) ); |
| 554 | - | |
| 555 | 687 | return array(); |
| 556 | 688 | } |
| 557 | 689 | |
| 558 | 690 | return $params; |
| @@ -572,9 +704,9 @@ | ||
| 572 | 704 | $wp_base = get_home_url( null, '/', 'relative' ); |
| 573 | 705 | if ( substr( $request_path, 0, strlen( $wp_base ) ) === $wp_base ) { |
| 574 | 706 | $request_path = substr( $request_path, strlen( $wp_base ) ); |
| 575 | 707 | } |
| 576 | - $base_request_uri = rawurlencode( get_home_url( null, $request_path, is_ssl() ? 'https' : 'http' ) ); | |
| 708 | + $base_request_uri = rawurlencode( get_home_url( null, $request_path, $this->is_ssl_request() ? 'https' : 'http' ) ); | |
| 577 | 709 | |
| 578 | 710 | // Get the signature provided by the consumer and remove it from the parameters prior to checking the signature. |
| 579 | 711 | $consumer_signature = rawurldecode( str_replace( ' ', '+', $params['oauth_signature'] ) ); |
| 580 | 712 | unset( $params['oauth_signature'] ); |
| @@ -592,8 +724,21 @@ | ||
| 592 | 724 | if ( 'HMAC-SHA1' !== $params['oauth_signature_method'] && 'HMAC-SHA256' !== $params['oauth_signature_method'] ) { |
| 593 | 725 | return new WP_Error( 'mainwp_rest_authentication_error', __( 'Invalid signature - signature method is invalid.', 'mainwp' ), array( 'status' => 401 ) ); |
| 594 | 726 | } |
| 595 | 727 | |
| 728 | + // OAuth 1.0a HMAC verification requires the plaintext secret as the HMAC | |
| 729 | + // key. Keys created after MWP-1540 store the secret hashed at rest and | |
| 730 | + // therefore cannot be used with this auth method. Direct the caller to | |
| 731 | + // Basic Auth over HTTPS or to reissue (and use the new key only via | |
| 732 | + // Basic Auth). | |
| 733 | + // | |
| 734 | + // TODO MWP-1540 sunset (~v6.3): drop this branch. By the sunset target | |
| 735 | + // any plaintext keys still in the wild will already be failing through | |
| 736 | + // Basic Auth as well, so reissue is the only path forward anyway. | |
| 737 | + if ( ! $this->is_legacy_plaintext_secret( $user->consumer_secret ) ) { | |
| 738 | + return new WP_Error( 'mainwp_rest_authentication_oauth1_unsupported', __( 'OAuth 1.0a is not supported for this API key. Use Basic Auth over HTTPS or reissue the key.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 739 | + } | |
| 740 | + | |
| 596 | 741 | $hash_algorithm = strtolower( str_replace( 'HMAC-', '', $params['oauth_signature_method'] ) ); |
| 597 | 742 | $secret = $user->consumer_secret . '&'; |
| 598 | 743 | $signature = base64_encode( hash_hmac( $hash_algorithm, $string_to_sign, $secret, true ) ); //phpcs:ignore -- ok. |
| 599 | 744 | |
| @@ -701,8 +846,10 @@ | ||
| 701 | 846 | array( '%s' ), |
| 702 | 847 | array( '%d' ) |
| 703 | 848 | ); |
| 704 | 849 | |
| 850 | + wp_cache_delete( 'mainwp_api_key_' . md5( $user->consumer_key ) ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 851 | + | |
| 705 | 852 | return true; |
| 706 | 853 | } |
| 707 | 854 | |
| 708 | 855 | /** |
| @@ -708,30 +855,65 @@ | ||
| 708 | 855 | /** |
| 709 | 856 | * Return the user data for the given consumer_key. |
| 710 | 857 | * |
| 711 | 858 | * @param string $consumer_key Consumer key. |
| 712 | - * @return array | |
| 859 | + * @return mixed | |
| 713 | 860 | */ |
| 714 | 861 | private function get_user_data_by_consumer_key( $consumer_key ) { |
| 715 | 862 | global $wpdb; |
| 716 | 863 | |
| 717 | 864 | $consumer_key = \mainwp_api_hash( sanitize_text_field( $consumer_key ) ); |
| 718 | - $user = $wpdb->get_row( | |
| 719 | - $wpdb->prepare( | |
| 720 | - ' | |
| 721 | - SELECT key_id, user_id, permissions, consumer_key, consumer_secret, nonces, enabled FROM ' . | |
| 722 | - MainWP_DB::instance()->get_table_name( 'api_keys' ) . ' | |
| 723 | - WHERE consumer_key = %s | |
| 724 | - ', | |
| 725 | - $consumer_key | |
| 726 | - ) | |
| 727 | - ); | |
| 865 | + $cache_key = 'mainwp_api_key_' . md5( $consumer_key ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 866 | + $user = wp_cache_get( $cache_key ); | |
| 728 | 867 | |
| 868 | + if ( false === $user ) { | |
| 869 | + $table_name = esc_sql( MainWP_DB::instance()->get_table_name( 'api_keys' ) ); | |
| 870 | + $user = $wpdb->get_row( | |
| 871 | + $wpdb->prepare( | |
| 872 | + 'SELECT * FROM ' . $table_name . ' WHERE consumer_key = %s', | |
| 873 | + $consumer_key | |
| 874 | + ) | |
| 875 | + ); | |
| 876 | + if ( $user ) { | |
| 877 | + wp_cache_set( $cache_key, $user ); | |
| 878 | + } | |
| 879 | + } | |
| 880 | + | |
| 729 | 881 | if ( empty( $user->enabled ) ) { |
| 730 | 882 | $this->set_error( new \WP_Error( 'mainwp_rest_authentication_disabled_key', __( 'The REST API Key are disabled.', 'mainwp' ), array( 'status' => 401 ) ) ); |
| 731 | 883 | return false; |
| 732 | 884 | } |
| 733 | 885 | |
| 886 | + // Legacy: passphrase enforcement for any rows where key_type=1. | |
| 887 | + // No current code path creates such rows (see MWP-1544) and no caller | |
| 888 | + // of insert_rest_api_key sets key_type to 1, so this branch is dormant | |
| 889 | + // on every install we have visibility into. It is preserved to avoid | |
| 890 | + // silently weakening hypothetical pre-existing rows where someone | |
| 891 | + // wired key_type=1 by hand or via removed migration code. The compare | |
| 892 | + // is timing-safe (was a vanilla !== before MWP-1540) and we leave the | |
| 893 | + // submitted value unsanitized so that a passphrase containing | |
| 894 | + // whitespace or HTML-like content still matches a legitimately | |
| 895 | + // stored value. | |
| 896 | + // | |
| 897 | + // TODO MWP-1544 sunset (~v6.3): remove this entire block. Nothing in | |
| 898 | + // the dashboard creates key_type=1 rows anymore, and the column | |
| 899 | + // itself can be dropped at the same time. | |
| 900 | + if ( 1 === (int) $user->key_type ) { | |
| 901 | + // The submitted value is only fed into hash_equals against the | |
| 902 | + // stored passphrase; it is never echoed, stored, or used in a | |
| 903 | + // query, so sanitization is intentionally skipped to preserve | |
| 904 | + // an exact match against legacy plaintext values that may | |
| 905 | + // contain whitespace or HTML-like characters. | |
| 906 | + // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 907 | + $pass = ! empty( $_REQUEST['key_pass'] ) ? wp_unslash( $_REQUEST['key_pass'] ) : ''; | |
| 908 | + // phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized | |
| 909 | + $expected = isset( $user->key_pass ) ? (string) $user->key_pass : ''; | |
| 910 | + if ( '' === $expected || ! hash_equals( $expected, $pass ) ) { | |
| 911 | + $this->set_error( new \WP_Error( 'mainwp_rest_authentication_invalid_key_pass', __( 'The REST API passphrase is invalid.', 'mainwp' ), array( 'status' => 401 ) ) ); | |
| 912 | + return false; | |
| 913 | + } | |
| 914 | + } | |
| 915 | + | |
| 734 | 916 | return $user; |
| 735 | 917 | } |
| 736 | 918 | |
| 737 | 919 | |
| @@ -742,32 +924,42 @@ | ||
| 742 | 924 | * @return bool|WP_Error |
| 743 | 925 | */ |
| 744 | 926 | private function check_permissions( $method ) { |
| 745 | 927 | $permissions = $this->user->permissions; |
| 746 | - | |
| 928 | + $msg = ''; | |
| 929 | + $flag = true; | |
| 747 | 930 | switch ( $method ) { |
| 748 | 931 | case 'HEAD': |
| 749 | 932 | case 'GET': |
| 750 | 933 | if ( 'read' !== $permissions && 'read_write' !== $permissions ) { |
| 751 | - return new WP_Error( 'mainwp_rest_authentication_error', __( 'The API key provided does not have read permissions.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 934 | + $msg = __( 'The API key provided does not have read permissions.', 'mainwp' ); | |
| 752 | 935 | } |
| 753 | 936 | break; |
| 754 | 937 | case 'POST': |
| 755 | 938 | case 'PUT': |
| 756 | 939 | case 'PATCH': |
| 940 | + if ( 'write' !== $permissions && 'read_write' !== $permissions ) { | |
| 941 | + $msg = __( 'The API key provided does not have write permissions.', 'mainwp' ); | |
| 942 | + } | |
| 943 | + break; | |
| 757 | 944 | case 'DELETE': |
| 758 | - if ( 'write' !== $permissions && 'read_write' !== $permissions ) { | |
| 759 | - return new WP_Error( 'mainwp_rest_authentication_error', __( 'The API key provided does not have write permissions.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 945 | + if ( 'write' !== $permissions && 'read_write' !== $permissions && 'delete' !== $permissions ) { | |
| 946 | + $msg = __( 'The API key provided does not have delete permissions.', 'mainwp' ); | |
| 760 | 947 | } |
| 761 | 948 | break; |
| 762 | 949 | case 'OPTIONS': |
| 763 | - return true; | |
| 950 | + $flag = true; | |
| 951 | + break; | |
| 952 | + default: | |
| 953 | + $msg = __( 'Unknown request method.', 'mainwp' ); | |
| 954 | + } | |
| 764 | 955 | |
| 765 | - default: | |
| 766 | - return new WP_Error( 'mainwp_rest_authentication_error', __( 'Unknown request method.', 'mainwp' ), array( 'status' => 401 ) ); | |
| 956 | + if ( ! empty( $msg ) ) { | |
| 957 | + return new WP_Error( 'mainwp_rest_authentication_error', $msg, array( 'status' => 401 ) ); | |
| 958 | + | |
| 767 | 959 | } |
| 768 | 960 | |
| 769 | - return true; | |
| 961 | + return $flag; | |
| 770 | 962 | } |
| 771 | 963 | |
| 772 | 964 | /** |
| 773 | 965 | * Updated API Key last access datetime. |
| @@ -794,8 +986,10 @@ | ||
| 794 | 986 | array( 'key_id' => $this->user->key_id ), |
| 795 | 987 | array( '%s' ), |
| 796 | 988 | array( '%d' ) |
| 797 | 989 | ); |
| 990 | + | |
| 991 | + wp_cache_delete( 'mainwp_api_key_' . md5( $this->user->consumer_key ) ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes. | |
| 798 | 992 | } |
| 799 | 993 | |
| 800 | 994 | /** |
| 801 | 995 | * If the consumer_key and consumer_secret $_GET parameters are NOT provided |
| @@ -842,15 +1036,32 @@ | ||
| 842 | 1036 | return $result; |
| 843 | 1037 | } |
| 844 | 1038 | |
| 845 | 1039 | /** |
| 1040 | + * Method get_rest_valid_user(). | |
| 1041 | + * | |
| 1042 | + * @return mixed|object User api key object. | |
| 1043 | + */ | |
| 1044 | + public function get_rest_valid_user() { | |
| 1045 | + return $this->user; | |
| 1046 | + } | |
| 1047 | + | |
| 1048 | + /** | |
| 846 | 1049 | * Valid REST permissions. |
| 847 | 1050 | * |
| 848 | 1051 | * @param WP_REST_Request $request Request used to generate the response. |
| 849 | 1052 | * |
| 850 | - * @return objext user rest data. | |
| 1053 | + * @return bool|\WP_Error True if valid, WP_Error otherwise. | |
| 851 | 1054 | */ |
| 852 | 1055 | public function is_valid_permissions( $request ) { |
| 1056 | + // If no user is authenticated, deny permission. | |
| 1057 | + if ( null === $this->user ) { | |
| 1058 | + return new WP_Error( | |
| 1059 | + 'mainwp_rest_authentication_error', | |
| 1060 | + __( 'You are not authenticated.', 'mainwp' ), | |
| 1061 | + array( 'status' => 401 ) | |
| 1062 | + ); | |
| 1063 | + } | |
| 853 | 1064 | return $this->check_permissions( $request->get_method() ); |
| 854 | 1065 | } |
| 855 | 1066 | } |
| 856 | 1067 | |