| @@ -45,16 +45,29 @@ | ||
| 45 | 45 | ); |
| 46 | 46 | } |
| 47 | 47 | |
| 48 | 48 | /** |
| 49 | - * Handles login request | |
| 49 | + * Handles login request. | |
| 50 | 50 | * |
| 51 | + * Authentication is delegated to wp_signon() so that the request runs | |
| 52 | + * through WordPress's `authenticate` filter chain. This lets brute-force | |
| 53 | + * protection plugins (Wordfence, Login LockDown, Jetpack Protect, Solid | |
| 54 | + * Security, etc.) intercept and block attempts, and ensures the core | |
| 55 | + * `wp_login_failed` action fires on failure so plugins that hook it (Limit | |
| 56 | + * Login Attempts, etc.) can count the attempt. Core authentication failures | |
| 57 | + * (wrong password, unknown username/email) return a single generic response | |
| 58 | + * so the endpoint cannot be used to enumerate valid accounts. A lockout from | |
| 59 | + * a protection plugin is surfaced as a 429 with that plugin's message so a | |
| 60 | + * rate-limited user understands why the login was refused. | |
| 61 | + * | |
| 62 | + * @since 4.15.5 Route through wp_signon() and return a generic failure | |
| 63 | + * response to prevent brute-force-protection bypass (CWE-307) | |
| 64 | + * and user enumeration (CWE-204); surface lockout messages as 429. | |
| 51 | 65 | * @since 2.10.0 |
| 52 | 66 | * |
| 53 | 67 | * @param WP_REST_Request $request |
| 54 | 68 | * |
| 55 | - * @return array | |
| 56 | - * | |
| 69 | + * @return WP_REST_Response | |
| 57 | 70 | */ |
| 58 | 71 | public function handleRequest(WP_REST_Request $request) |
| 59 | 72 | { |
| 60 | 73 | $login = $request->get_param('login'); |
| @@ -59,51 +72,82 @@ | ||
| 59 | 72 | { |
| 60 | 73 | $login = $request->get_param('login'); |
| 61 | 74 | $password = $request->get_param('password'); |
| 62 | 75 | |
| 63 | - $user = get_user_by('login', $login); | |
| 76 | + // wp_signon() runs the `authenticate` filter chain (so lockout plugins | |
| 77 | + // can intercept and block), fires `wp_login_failed` on failure, and sets | |
| 78 | + // the auth cookie on success. | |
| 79 | + $user = wp_signon( | |
| 80 | + [ | |
| 81 | + 'user_login' => $login, | |
| 82 | + 'user_password' => $password, | |
| 83 | + 'remember' => false, | |
| 84 | + ] | |
| 85 | + ); | |
| 64 | 86 | |
| 65 | - if ( ! $user) { | |
| 66 | - $user = get_user_by('email', $login); | |
| 67 | - } | |
| 87 | + if (is_wp_error($user)) { | |
| 88 | + // Core authentication errors (wrong password, unknown username/email) | |
| 89 | + // are collapsed into a single generic message so the endpoint cannot | |
| 90 | + // be used to enumerate accounts. Any other error code originates from | |
| 91 | + // a brute-force protection plugin (lockout) — surface its message so | |
| 92 | + // a locked-out user understands why the login was refused. | |
| 93 | + $coreAuthErrorCodes = [ | |
| 94 | + 'incorrect_password', | |
| 95 | + 'invalid_username', | |
| 96 | + 'invalid_email', | |
| 97 | + 'empty_username', | |
| 98 | + 'empty_password', | |
| 99 | + ]; | |
| 68 | 100 | |
| 69 | - if ($user) { | |
| 70 | - if (wp_check_password($password, $user->user_pass, $user->ID)) { | |
| 71 | - give_log_user_in($user->ID, $login, $password); | |
| 101 | + $isLockout = ! in_array($user->get_error_code(), $coreAuthErrorCodes, true); | |
| 72 | 102 | |
| 103 | + // Note: the logical result is returned in the body `status` field and | |
| 104 | + // the HTTP status is left at 200, matching the endpoint's original | |
| 105 | + // contract. The Donor Dashboard front-end reads the result from the | |
| 106 | + // body; returning a non-2xx HTTP status would make its Axios request | |
| 107 | + // reject and leave the form stuck in a loading state. | |
| 108 | + if ($isLockout) { | |
| 109 | + $lockoutMessage = wp_strip_all_tags($user->get_error_message()); | |
| 110 | + | |
| 73 | 111 | return new WP_REST_Response( |
| 74 | 112 | [ |
| 75 | - 'status' => 200, | |
| 76 | - 'response' => 'login_successful', | |
| 113 | + 'status' => 429, | |
| 114 | + 'response' => 'too_many_attempts', | |
| 77 | 115 | 'body_response' => [ |
| 78 | - 'login' => $user->login, | |
| 79 | - 'id' => $user->ID, | |
| 116 | + 'message' => '' !== $lockoutMessage | |
| 117 | + ? $lockoutMessage | |
| 118 | + : __('Too many failed login attempts. Please try again later.', 'give'), | |
| 80 | 119 | ], |
| 81 | 120 | ] |
| 82 | 121 | ); |
| 83 | - } else { | |
| 84 | - return new WP_REST_Response( | |
| 85 | - [ | |
| 86 | - 'status' => 400, | |
| 87 | - 'response' => 'incorrect_password', | |
| 88 | - 'body_response' => [ | |
| 89 | - 'message' => __('The provided password was incorrect.', 'give'), | |
| 90 | - ], | |
| 91 | - ] | |
| 92 | - ); | |
| 93 | 122 | } |
| 94 | - } else { | |
| 123 | + | |
| 95 | 124 | return new WP_REST_Response( |
| 96 | 125 | [ |
| 97 | - 'status' => 400, | |
| 98 | - 'response' => 'unidentified_login', | |
| 126 | + 'status' => 401, | |
| 127 | + 'response' => 'login_failed', | |
| 99 | 128 | 'body_response' => [ |
| 100 | - 'message' => sprintf( | |
| 101 | - __('A record for the provided login (%s) could not be found.', 'give'), | |
| 102 | - $login | |
| 103 | - ), | |
| 129 | + 'message' => __('The provided credentials are invalid.', 'give'), | |
| 104 | 130 | ], |
| 105 | 131 | ] |
| 106 | 132 | ); |
| 107 | 133 | } |
| 134 | + | |
| 135 | + // wp_signon() sets the auth cookie and fires `wp_login`, but does not set | |
| 136 | + // the current user for the active request. Set it so the rest of this | |
| 137 | + // request is authenticated, then fire only GiveWP's own hook so existing | |
| 138 | + // integrations keep working, without duplicating the cookie or `wp_login`. | |
| 139 | + wp_set_current_user($user->ID, $user->user_login); | |
| 140 | + do_action('give_log_user_in', $user->ID, $user->user_login, $password); | |
| 141 | + | |
| 142 | + return new WP_REST_Response( | |
| 143 | + [ | |
| 144 | + 'status' => 200, | |
| 145 | + 'response' => 'login_successful', | |
| 146 | + 'body_response' => [ | |
| 147 | + 'login' => $user->user_login, | |
| 148 | + 'id' => $user->ID, | |
| 149 | + ], | |
| 150 | + ] | |
| 151 | + ); | |
| 108 | 152 | } |
| 109 | 153 | } |