PluginProbe
The WP Remote WordPress Plugin / 6.76
The WP Remote WordPress Plugin v6.76
6.76 6.72 6.69 6.65 6.62 6.48 6.47 4.87 4.97 5.05 5.09 5.16 5.22 5.24 5.25 5.38 5.41 5.42 5.45 5.47 5.53 5.56 5.65 5.68 5.72 All 54 releases
← All changes | wp_2fa/wp_2fa.php +95 -223 6.486.76 View file →
@@ -1,38 +1,57 @@
1 1 <?php
2 2 if (!defined('ABSPATH')) exit;
3 3 if (!class_exists('WPRWP2FA')) :
4 4
5 -require_once dirname(__FILE__) . '/authenticator.php';
6 5 require_once dirname(__FILE__) . '/utils.php';
6 +require_once dirname(__FILE__) . '/time_otp.php';
7 +require_once dirname(__FILE__) . '/time_otp_login.php';
8 +require_once dirname(__FILE__) . '/email_otp.php';
9 +require_once dirname(__FILE__) . '/email_otp_template.php';
10 +require_once dirname(__FILE__) . '/email_otp_sender.php';
11 +require_once dirname(__FILE__) . '/email_otp_login.php';
7 12
8 13 class WPRWP2FA {
9 14 const FLAG_META_KEY = 'wpr_2fa_enabled';
10 15 const SECRET_META_KEY = 'wpr_2fa_secret';
16 + const METHOD_META_KEY = 'wpr_2fa_method';
17 + const EMAIL_CHALLENGE_META_KEY = 'wpr_2fa_email_challenge';
18 + const EMAIL_RATE_META_KEY = 'wpr_2fa_email_rate';
19 + const ATTEMPTS_META_KEY = 'wpr_2fa_attempts';
11 20 const INVALID_CODE_MESSAGE = 'The 2FA code you entered is incorrect.';
12 21 const TOOLTIP_MESSAGE = 'Please contact your administrator if you need assistance.';
22 + const CONTEXT_MESSAGE = 'Two-factor authentication is required in the standard sign-in page.';
23 + const CONFIG_MESSAGE = 'Please contact your administrator to login.';
13 24
14 25 public static $cipher_algo = 'aes-256-cbc';
15 26 public static $wp_2fa_option = 'wprWp2faConf';
16 27
17 - private $bvinfo;
18 - private $settings;
19 - private $invalid_code_message = self::INVALID_CODE_MESSAGE;
20 - private $tooltip_message = self::TOOLTIP_MESSAGE;
28 + private static $whitelabel = null;
21 29
22 - public function __construct() {
23 - $this->settings = new WPRWPSettings();
24 - $this->bvinfo = new WPRInfo($this->settings);
30 + public static function whitelabelMessage($key, $default) {
31 + if (self::$whitelabel === null) {
32 + $info = new WPRInfo(new WPRWPSettings());
33 + $values = $info->getLPWhitelabelInfo();
34 + self::$whitelabel = is_array($values) ? $values : array();
35 + }
25 36
26 - $whitelabel_info = $this->bvinfo->getLPWhitelabelInfo();
37 + return (isset(self::$whitelabel[$key]) && is_string(self::$whitelabel[$key])) ? self::$whitelabel[$key] : $default;
38 + }
27 39
28 - if (isset($whitelabel_info['2fa_error_message']) && is_string($whitelabel_info['2fa_error_message'])) {
29 - $this->invalid_code_message = $whitelabel_info['2fa_error_message'];
40 + # Sent as a JSON failure rather than returned as a WP_Error. These are system
41 + # states, not wrong credentials, and a WP_Error would be recorded as a failed
42 + # login against the firewall's lockout counter.
43 + public static function sendFailure($message, $data = array()) {
44 + wp_send_json_error(array_merge($data, array('message' => $message)));
45 + exit;
46 + }
47 +
48 + public static function humanWait($seconds) {
49 + if ($seconds < 90) {
50 + return sprintf('%d seconds', max(1, intval($seconds)));
30 51 }
31 52
32 - if (isset($whitelabel_info['2fa_tooltip']) && is_string($whitelabel_info['2fa_tooltip'])) {
33 - $this->tooltip_message = $whitelabel_info['2fa_tooltip'];
34 - }
53 + return sprintf('%d minutes', intval(ceil($seconds / MINUTE_IN_SECONDS)));
35 54 }
36 55
37 56 public static function isEnabled($settings) {
38 57 $config = $settings->getOption(self::$wp_2fa_option);
@@ -41,15 +60,16 @@
41 60 $config['enabled'] === true);
42 61 }
43 62
44 63 public function init() {
45 - add_action('wp_enqueue_scripts', array($this, 'enqueue_dashicons'));
46 64 add_filter('authenticate', array($this, 'authenticate'), 25, 3);
47 65 add_action('login_form', array($this, 'custom_login_form'));
66 + add_action('login_enqueue_scripts', array($this, 'enqueue_login_assets'));
48 67 }
49 68
50 - public function enqueue_dashicons() {
51 - wp_enqueue_style('dashicons');
69 + public function enqueue_login_assets() {
70 + wp_enqueue_style('WPR-wp-2fa-login', plugin_dir_url(__FILE__) . 'css/login.css', array(), '1.4');
71 + wp_enqueue_script('WPR-wp-2fa-login', plugin_dir_url(__FILE__) . 'js/login.js', array(), '1.4', true);
52 72 }
53 73
54 74 public function authenticate($user, $username, $password) {
55 75 if (!($user instanceof WP_User)) {
@@ -55,236 +75,88 @@
55 75 if (!($user instanceof WP_User)) {
56 76 return $user;
57 77 }
58 78
59 - $has_2fa = get_user_meta($user->ID, WPRWP2FA::FLAG_META_KEY, true);
79 + if ('1' !== get_user_meta($user->ID, self::FLAG_META_KEY, true)) {
80 + return $user;
81 + }
60 82
61 - if ('1' === $has_2fa) {
62 - if (empty($_POST['twofa_code'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
63 - wp_send_json_success(array('twofa_enabled' => true));
64 - exit;
65 - } else {
66 - $encoded_secret_info = get_user_meta($user->ID, WPRWP2FA::SECRET_META_KEY, true);
83 + if (!self::isInteractiveLogin()) {
84 + return new WP_Error('twofa_context', self::CONTEXT_MESSAGE);
85 + }
67 86
68 - $secret_info = WPRWP2FAUtils::getSecretInfo($encoded_secret_info);
69 - $secret = $secret_info['secret'];
70 - $is_secret_encrypted = $secret_info['is_encrypted'];
87 + $method = get_user_meta($user->ID, self::METHOD_META_KEY, true);
88 + if ($method === 'email_otp') {
89 + return WPRWP2FAEmailOTPLogin::authenticate($user);
90 + }
91 + if ($method !== '' && $method !== 'totp') {
92 + self::sendFailure(self::CONFIG_MESSAGE);
93 + }
71 94
72 - if (is_null($secret) || is_null($is_secret_encrypted)) {
73 - return new WP_Error('invalid_2fa_configuration', 'Please contact your administrator to login.');
74 - }
95 + return WPRWP2FATimeOTPLogin::authenticate($user);
96 + }
75 97
76 - if (defined('SECURE_AUTH_KEY') && $is_secret_encrypted === true) {
77 - $decryption_result = WPRHelper::opensslDecrypt($secret, self::$cipher_algo, SECURE_AUTH_KEY);
78 - if ($decryption_result[0] === false) {
79 - return new WP_Error('2fa_secret_key_decryption_error', 'Please contact your administrator to login.');
80 - }
81 - $secret = $decryption_result[1];
82 - }
98 + private static function isInteractiveLogin() {
99 + global $pagenow;
83 100
84 - if (empty($secret) || !is_string($secret) || 32 !== strlen($secret)) {
85 - return new WP_Error('invalid_2fa_configuration', 'Please contact your administrator to login.');
86 - }
87 -
88 - $submitted_code = WPRHelper::getRawParam('POST', 'twofa_code');
89 -
90 - if (is_string($submitted_code) && ctype_digit($submitted_code) &&
91 - true === WPRWP2FAAuthenticator::verifyCode($secret, $submitted_code)) {
92 -
93 - return $user;
94 - } else {
95 - return new WP_Error('invalid_2fa_code', esc_html($this->invalid_code_message));
96 - }
97 - }
101 + if ((defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) ||
102 + (defined('REST_REQUEST') && REST_REQUEST) ||
103 + (defined('WP_CLI') && WP_CLI)) {
104 + return false;
98 105 }
99 106
100 - return $user;
107 + return $pagenow === 'wp-login.php';
101 108 }
102 109
103 110 function custom_login_form() {
104 - $tooltip_message = $this->tooltip_message;
111 + $tooltip_message = self::whitelabelMessage('2fa_tooltip', self::TOOLTIP_MESSAGE);
105 112 $is_url = filter_var($tooltip_message, FILTER_VALIDATE_URL);
113 + $allowed_tooltip_html = array(
114 + 'a' => array(
115 + 'class' => true,
116 + 'href' => true,
117 + 'rel' => true,
118 + 'target' => true
119 + ),
120 + 'span' => array(
121 + 'class' => true,
122 + 'id' => true,
123 + 'title' => true
124 + )
125 + );
106 126
107 - $icon_css = 'font-size: 20px; color: #2271b1; cursor: pointer;';
108 127 $icon_html = '<span
109 - id="twofa_help_icon"
110 - class="dashicons dashicons-editor-help"
111 - style="' . esc_attr($icon_css) . '"></span>';
128 + id="twofa-help-icon"
129 + class="dashicons dashicons-editor-help"></span>';
112 130
113 131 if ($is_url) {
114 132 $tooltip_html = '<a
115 - href="' . esc_url($tooltip_message) . '"
116 - target="_blank"
117 - style="text-decoration: none;">' . $icon_html . '</a>';
133 + href="' . esc_url($tooltip_message) . '"
134 + target="_blank"
135 + rel="noopener noreferrer"
136 + class="twofa-help-link">' . $icon_html . '</a>';
118 137 } else {
119 138 $tooltip_html = '<span
120 - id="twofa_help_icon"
121 - class="dashicons dashicons-editor-help"
122 - title="' . esc_attr($tooltip_message) . '"
123 - style="' . esc_attr($icon_css) . '"></span>';
139 + id="twofa-help-icon"
140 + class="dashicons dashicons-editor-help"
141 + title="' . esc_attr($tooltip_message) . '"></span>';
124 142 }
125 - // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- not the best way
126 143 ?>
127 - <style>
128 - .wp2fa-progress-bar {
129 - width: 100%;
130 - background-color: #f3f3f3;
131 - display: none;
132 - margin-bottom: 10px;
133 - }
134 -
135 - .wp2fa-progress-bar.show {
136 - display: block;
137 - }
138 -
139 - .wp2fa-progress-bar .progress-bar-inner {
140 - width: 0;
141 - height: 5px;
142 - background-color: #2271b1;
143 - animation: loader 1s ease infinite;
144 - }
145 -
146 - @keyframes loader {
147 - 100% {width: 100%}
148 - }
149 - </style>
150 -
151 144 <div class="wp2fa-progress-bar">
152 145 <div class="progress-bar-inner"></div>
153 146 </div>
154 -
155 - <script type="text/javascript">
156 - document.addEventListener('DOMContentLoaded', function() {
157 - const loginForm = document.getElementById('loginform');
158 - const usernameField = document.getElementById('user_login');
159 - const passwordField = document.getElementById('user_pass');
160 - const loginButton = document.getElementById('wp-submit');
161 - let loginError = document.getElementById('login_error');
162 - let isTwoFAEnabled = false;
163 - const progressBar = document.getElementsByClassName('wp2fa-progress-bar')[0];
164 -
165 - if (loginForm && usernameField && passwordField && loginButton) {
166 - loginForm.addEventListener('submit', handleSubmit);
167 - }
168 -
169 - function handleSubmit(event) {
170 - event.preventDefault();
171 - showProgressBar();
172 - disableLoginButton();
173 -
174 - const formData = new FormData(loginForm);
175 -
176 - fetch(loginForm.action, {
177 - method: 'POST',
178 - body: formData,
179 - credentials: 'same-origin'
180 - }).then(response => response.text())
181 - .then(text => {
182 - try {
183 - return JSON.parse(text);
184 - } catch (e) {
185 - return { success: false, html: text };
186 - }
187 - })
188 - .then(data => {
189 - if (data.success && data.data && data.data.twofa_enabled) {
190 - isTwoFAEnabled = true;
191 - showTwoFAField();
192 - clearLoginError();
193 - } else {
194 - if (data.html) {
195 - handleHtmlResponse(data.html);
196 - } else if (data.data && data.data.message) {
197 - displayError(data.data.message);
198 - } else {
199 - displayError('An unknown error occurred');
200 - }
201 - if (isTwoFAEnabled) {
202 - showTwoFAField();
203 - }
204 - }
205 - })
206 - .catch(error => {
207 - displayError('An error occurred while processing your request');
208 - if (isTwoFAEnabled) {
209 - showTwoFAField();
210 - }
211 - })
212 - .finally(() => {
213 - hideProgressBar();
214 - enableLoginButton();
215 - })
216 - }
217 -
218 - function handleHtmlResponse(html) {
219 - const parser = new DOMParser();
220 - const doc = parser.parseFromString(html, 'text/html');
221 - const errorElement = doc.getElementById('login_error');
222 - if (errorElement) {
223 - displayError(errorElement.innerText.trim());
224 - } else {
225 - proceedWithLogin();
226 - }
227 - }
228 -
229 - function showTwoFAField() {
230 - let twofaField = document.getElementById('twofa_code_field');
231 - if (!twofaField) {
232 - twofaField = document.createElement('p');
233 - twofaField.id = 'twofa_code_field';
234 - twofaField.innerHTML = `
235 - <label for="twofa_code" style="position: relative; display: block;">
236 - 2FA Code
237 - <?php echo $tooltip_html; ?>
238 - </label>
239 - <input type="text" required name="twofa_code" id="twofa_code" class="input" value="" maxlength="6" minlength="6">
240 - `;
241 - passwordField.parentNode.insertBefore(twofaField, passwordField.nextSibling);
242 - }
243 - twofaField.style.display = 'block';
244 - document.getElementById('twofa_code').value = '';
245 - }
246 -
247 - function clearLoginError() {
248 - if (loginError) {
249 - loginError.style.display = 'none';
250 - }
251 - }
252 -
253 - function displayError(message) {
254 - if (!loginError) {
255 - loginError = document.createElement('div');
256 - loginError.id = 'login_error';
257 - loginForm.parentNode.insertBefore(loginError, loginForm);
258 - }
259 - loginError.textContent = message;
260 - loginError.style.display = 'block';
261 - }
262 -
263 - function proceedWithLogin() {
264 - loginForm.removeEventListener('submit', handleSubmit);
265 - loginForm.submit();
266 - }
267 -
268 - function showProgressBar() {
269 - progressBar.classList.add('show');
270 - }
271 -
272 - function hideProgressBar() {
273 - progressBar.classList.remove('show');
274 - }
275 -
276 - function disableLoginButton() {
277 - loginButton.disabled = true;
278 - loginButton.value = 'Verifying...';
279 - }
280 -
281 - function enableLoginButton() {
282 - loginButton.disabled = false;
283 - loginButton.value = 'Log In';
284 - }
285 - });
286 - </script>
147 + <template id="twofa-field-template">
148 + <p id="twofa-code-field">
149 + <label for="twofa-code">
150 + 2FA Code
151 + <?php echo wp_kses($tooltip_html, $allowed_tooltip_html); ?>
152 + </label>
153 + <input type="text" required name="twofa_code" id="twofa-code" class="input" value="" maxlength="6" minlength="6" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
154 + <span id="twofa-destination" role="status" aria-live="polite"></span>
155 + <button type="button" id="twofa-resend" class="button-link">Send a new code</button>
156 + <span id="twofa-resend-status" role="status" aria-live="polite"></span>
157 + </p>
158 + </template>
287 159 <?php
288 160 }
289 161 }
290 -endif;
162 +endif;