| @@ -45,10 +45,20 @@ | ||
| 45 | 45 | __('<strong>Error:</strong> This email address is already registered. Please login or try resetting your password.', 'fluent-community') |
| 46 | 46 | ); |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | + /** | |
| 50 | + * MemberPress rejects every `register_post` while its "Disable WordPress registration form" | |
| 51 | + * option is on (default on). That option targets wp-login.php, not the community portal, which has its own registration gate. | |
| 52 | + */ | |
| 53 | + $hadMeprBlocker = remove_action('register_post', 'MeprUsersCtrl::maybe_disable_wp_registration_form', 10); | |
| 54 | + | |
| 49 | 55 | do_action('register_post', $sanitized_user_login, $user_email, $errors); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound |
| 50 | 56 | |
| 57 | + if ($hadMeprBlocker) { | |
| 58 | + add_action('register_post', 'MeprUsersCtrl::maybe_disable_wp_registration_form', 10, 3); | |
| 59 | + } | |
| 60 | + | |
| 51 | 61 | if ($errors->has_errors()) { |
| 52 | 62 | return $errors; |
| 53 | 63 | } |
| 54 | 64 | |
| @@ -240,9 +250,11 @@ | ||
| 240 | 250 | } |
| 241 | 251 | |
| 242 | 252 | public static function isPasswordConfRequired() |
| 243 | 253 | { |
| 244 | - return apply_filters('fluent_community/autg/password_confirmation', true); | |
| 254 | + $isRequired = apply_filters_deprecated('fluent_community/autg/password_confirmation', [true], '2.7.8', 'fluent_community/auth/password_confirmation'); | |
| 255 | + | |
| 256 | + return apply_filters('fluent_community/auth/password_confirmation', $isRequired); | |
| 245 | 257 | } |
| 246 | 258 | |
| 247 | 259 | public static function isRegistrationEnabled() |
| 248 | 260 | { |
| @@ -273,23 +285,19 @@ | ||
| 273 | 285 | } catch (\Exception $e) { |
| 274 | 286 | $verifcationCode = str_pad((string) wp_rand(100123, 900987), 6, '0', STR_PAD_LEFT); |
| 275 | 287 | } |
| 276 | 288 | |
| 277 | - // Hash the code | |
| 289 | + // Keep the code hash server-side, keyed by an opaque challenge id. The client only ever | |
| 290 | + // receives the id, never the password verifier, so the code cannot be recovered offline. | |
| 278 | 291 | $codeHash = wp_hash_password($verifcationCode); |
| 279 | - | |
| 280 | - // Create a token with the email and code hash | |
| 281 | - $data = [ | |
| 292 | + $signedToken = 'fcs_' . wp_generate_password(40, false); | |
| 293 | + set_transient('fcom_signup_2fa_' . $signedToken, [ | |
| 282 | 294 | 'email' => $formData['email'], |
| 283 | 295 | 'code_hash' => $codeHash, |
| 284 | - 'expires' => time() + 600 // 10 minutes expiry | |
| 285 | - ]; | |
| 286 | - $token = base64_encode(json_encode($data)); | |
| 296 | + 'expires' => time() + 600, // 10 minutes expiry | |
| 297 | + 'attempts' => 0, | |
| 298 | + ], 600); | |
| 287 | 299 | |
| 288 | - // Sign the token | |
| 289 | - $signature = hash_hmac('sha256', $token, SECURE_AUTH_KEY); | |
| 290 | - $signedToken = $token . '.' . $signature; | |
| 291 | - | |
| 292 | 300 | /* translators: %s is replaced by the title of the site */ |
| 293 | 301 | $mailSubject = apply_filters("fluent_community/auth/signup_verification_mail_subject", sprintf(__('Your registration verification code for %s', 'fluent-community'), Arr::get($generalSettings, 'site_title'))); |
| 294 | 302 | |
| 295 | 303 | $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">'; |
| @@ -369,34 +377,21 @@ | ||
| 369 | 377 | } |
| 370 | 378 | |
| 371 | 379 | public static function validateVerificationCode($code, $verificationToken, $formData) |
| 372 | 380 | { |
| 373 | - if (!is_string($verificationToken) || strpos($verificationToken, '.') === false) { | |
| 381 | + if (!is_string($verificationToken) || $verificationToken === '') { | |
| 374 | 382 | return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); |
| 375 | 383 | } |
| 376 | 384 | |
| 377 | - list($data, $signature) = explode('.', $verificationToken, 2); | |
| 378 | - if (!$data || !$signature) { | |
| 379 | - return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); | |
| 380 | - } | |
| 385 | + $transientKey = 'fcom_signup_2fa_' . $verificationToken; | |
| 386 | + $data = get_transient($transientKey); | |
| 381 | 387 | |
| 382 | - $expectedSignature = hash_hmac('sha256', $data, SECURE_AUTH_KEY); | |
| 383 | - | |
| 384 | - if (!hash_equals($expectedSignature, $signature)) { | |
| 385 | - return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); | |
| 386 | - } | |
| 387 | - | |
| 388 | - $decodedData = base64_decode($data, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode | |
| 389 | - if ($decodedData === false) { | |
| 390 | - return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); | |
| 391 | - } | |
| 392 | - | |
| 393 | - $data = json_decode($decodedData, true); | |
| 394 | 388 | if (!is_array($data) || empty($data['expires']) || empty($data['email']) || empty($data['code_hash'])) { |
| 395 | 389 | return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community')); |
| 396 | 390 | } |
| 397 | 391 | |
| 398 | 392 | if ((int)$data['expires'] < time()) { |
| 393 | + delete_transient($transientKey); | |
| 399 | 394 | return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community')); |
| 400 | 395 | } |
| 401 | 396 | |
| 402 | 397 | if (!isset($formData['email']) || $data['email'] !== $formData['email']) { |
| @@ -402,11 +397,22 @@ | ||
| 402 | 397 | if (!isset($formData['email']) || $data['email'] !== $formData['email']) { |
| 403 | 398 | return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community')); |
| 404 | 399 | } |
| 405 | 400 | |
| 401 | + // Cap online guesses per challenge: after too many wrong codes the challenge is burned. | |
| 402 | + if ((int) Arr::get($data, 'attempts', 0) >= 10) { | |
| 403 | + delete_transient($transientKey); | |
| 404 | + return new \WP_Error('too_many_attempts', __('Too many invalid attempts. Please try again', 'fluent-community')); | |
| 405 | + } | |
| 406 | + | |
| 406 | 407 | if (!wp_check_password($code, $data['code_hash'])) { |
| 408 | + $data['attempts'] = (int) Arr::get($data, 'attempts', 0) + 1; | |
| 409 | + set_transient($transientKey, $data, max(1, (int) $data['expires'] - time())); | |
| 407 | 410 | return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community')); |
| 408 | 411 | } |
| 412 | + | |
| 413 | + // Single-use: consume the challenge on success. | |
| 414 | + delete_transient($transientKey); | |
| 409 | 415 | |
| 410 | 416 | return true; |
| 411 | 417 | } |
| 412 | 418 | |