PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | Modules/Auth/AuthHelper.php +117 -35 2.6.02.11.0 View file →
@@ -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
@@ -128,17 +138,90 @@
128 138
129 139 return $user;
130 140 }
131 141
142 + /**
143 + * The slug this plugin is known by inside FluentAuth.
144 + */
145 + const FLUENT_AUTH_HOST = 'fluent-community';
146 +
147 + /**
148 + * Whether FluentAuth's login stack is usable on this screen.
149 + *
150 + * Note the order this has to be asked in: adoptFluentAuth() is what makes the answer
151 + * yes on a site whose shortcode setting is off, so the auth screen adopts first and
152 + * asks second. Everywhere else - a plugin wondering whether the portal is running
153 + * FluentAuth's forms - the question stands on its own.
154 + */
132 155 public static function isFluentAuthAvailable()
133 156 {
134 - if (defined('FLUENT_AUTH_VERSION') && FLUENT_AUTH_VERSION) {
135 - return (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled();
157 + if (!defined('FLUENT_AUTH_VERSION') || !FLUENT_AUTH_VERSION) {
158 + return false;
136 159 }
137 160
138 - return false;
161 + return (new \FluentAuth\App\Hooks\Handlers\CustomAuthHandler())->isEnabled();
139 162 }
140 163
164 + /**
165 + * Tells FluentAuth this plugin exists, so it recognises the admin-ajax posts our
166 + * auth screen makes later. Cheap enough to run on every request, which is what it
167 + * has to do - the form post is a request of its own.
168 + *
169 + * @return void
170 + */
171 + public static function registerWithFluentAuth()
172 + {
173 + if (!self::hasFluentAuthBridge()) {
174 + return;
175 + }
176 +
177 + \FluentAuth\App\Services\LoginBridge::register(
178 + self::FLUENT_AUTH_HOST,
179 + 'is_fcom_auth',
180 + /*
181 + * Our own login endpoint. Unreachable while the portal renders FluentAuth's
182 + * form, but a site that filters the adoption back off falls through to it,
183 + * and this is what keeps the second factor arriving as a form there rather
184 + * than as the error message handleUserLogin() would otherwise print.
185 + */
186 + ['fcom_user_login_form']
187 + );
188 + }
189 +
190 + /**
191 + * Hands the screen being rendered to FluentAuth: its shortcodes render here even
192 + * where the site has the front end forms switched off, its assets load, and its
193 + * endpoints answer the posts this screen's forms make.
194 + *
195 + * @return bool whether FluentAuth took it
196 + */
197 + public static function adoptFluentAuth()
198 + {
199 + if (!self::hasFluentAuthBridge()) {
200 + return false;
201 + }
202 +
203 + \FluentAuth\App\Services\LoginBridge::adopt([
204 + 'host' => self::FLUENT_AUTH_HOST
205 + ]);
206 +
207 + return true;
208 + }
209 +
210 + /**
211 + * @return bool whether the installed FluentAuth is new enough to be adopted
212 + */
213 + private static function hasFluentAuthBridge()
214 + {
215 + // FluentAuth's autoloader requires the mapped file unconditionally, so probing
216 + // for a class an older release does not ship is a fatal error, not a false.
217 + return defined('FLUENT_AUTH_VERSION')
218 + && FLUENT_AUTH_VERSION
219 + && defined('FLUENT_AUTH_PLUGIN_PATH')
220 + && file_exists(FLUENT_AUTH_PLUGIN_PATH . 'app/Services/LoginBridge.php')
221 + && class_exists('\FluentAuth\App\Services\LoginBridge');
222 + }
223 +
141 224 public static function getTermsText()
142 225 {
143 226 $policyUrl = apply_filters('fluent_community/terms_policy_url', get_privacy_policy_url());
144 227
@@ -240,9 +323,11 @@
240 323 }
241 324
242 325 public static function isPasswordConfRequired()
243 326 {
244 - return apply_filters('fluent_community/autg/password_confirmation', true);
327 + $isRequired = apply_filters_deprecated('fluent_community/autg/password_confirmation', [true], '2.7.8', 'fluent_community/auth/password_confirmation');
328 +
329 + return apply_filters('fluent_community/auth/password_confirmation', $isRequired);
245 330 }
246 331
247 332 public static function isRegistrationEnabled()
248 333 {
@@ -258,9 +343,12 @@
258 343 }
259 344
260 345 public static function isTwoFactorEnabled()
261 346 {
262 - return apply_filters('fluent_auth/verify_signup_email', true);
347 + // fluent_auth/verify_signup_email is kept for backward compatibility with FluentAuth-targeted snippets
348 + $enabled = apply_filters('fluent_auth/verify_signup_email', true);
349 +
350 + return apply_filters('fluent_community/auth/two_factor_enabled', $enabled);
263 351 }
264 352
265 353 public static function get2FaRegistrationCodeForm($formData)
266 354 {
@@ -265,28 +353,24 @@
265 353 public static function get2FaRegistrationCodeForm($formData)
266 354 {
267 355 $generalSettings = Helper::generalSettings();
268 356 try {
269 - $verifcationCode = str_pad(random_int(100123, 900987), 6, 0, STR_PAD_LEFT);
357 + $verifcationCode = str_pad((string) random_int(100123, 900987), 6, '0', STR_PAD_LEFT);
270 358 } catch (\Exception $e) {
271 - $verifcationCode = str_pad(wp_rand(100123, 900987), 6, 0, STR_PAD_LEFT);
359 + $verifcationCode = str_pad((string) wp_rand(100123, 900987), 6, '0', STR_PAD_LEFT);
272 360 }
273 361
274 - // Hash the code
362 + // Keep the code hash server-side, keyed by an opaque challenge id. The client only ever
363 + // receives the id, never the password verifier, so the code cannot be recovered offline.
275 364 $codeHash = wp_hash_password($verifcationCode);
276 -
277 - // Create a token with the email and code hash
278 - $data = [
365 + $signedToken = 'fcs_' . wp_generate_password(40, false);
366 + set_transient('fcom_signup_2fa_' . $signedToken, [
279 367 'email' => $formData['email'],
280 368 'code_hash' => $codeHash,
281 - 'expires' => time() + 600 // 10 minutes expiry
282 - ];
283 - $token = base64_encode(json_encode($data));
369 + 'expires' => time() + 600, // 10 minutes expiry
370 + 'attempts' => 0,
371 + ], 600);
284 372
285 - // Sign the token
286 - $signature = hash_hmac('sha256', $token, SECURE_AUTH_KEY);
287 - $signedToken = $token . '.' . $signature;
288 -
289 373 /* translators: %s is replaced by the title of the site */
290 374 $mailSubject = apply_filters("fluent_community/auth/signup_verification_mail_subject", sprintf(__('Your registration verification code for %s', 'fluent-community'), Arr::get($generalSettings, 'site_title')));
291 375
292 376 $pStart = '<p style="font-family: Arial, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">';
@@ -366,34 +450,21 @@
366 450 }
367 451
368 452 public static function validateVerificationCode($code, $verificationToken, $formData)
369 453 {
370 - if (!is_string($verificationToken) || strpos($verificationToken, '.') === false) {
454 + if (!is_string($verificationToken) || $verificationToken === '') {
371 455 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
372 456 }
373 457
374 - list($data, $signature) = explode('.', $verificationToken, 2);
375 - if (!$data || !$signature) {
376 - return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
377 - }
458 + $transientKey = 'fcom_signup_2fa_' . $verificationToken;
459 + $data = get_transient($transientKey);
378 460
379 - $expectedSignature = hash_hmac('sha256', $data, SECURE_AUTH_KEY);
380 -
381 - if (!hash_equals($expectedSignature, $signature)) {
382 - return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
383 - }
384 -
385 - $decodedData = base64_decode($data, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
386 - if ($decodedData === false) {
387 - return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
388 - }
389 -
390 - $data = json_decode($decodedData, true);
391 461 if (!is_array($data) || empty($data['expires']) || empty($data['email']) || empty($data['code_hash'])) {
392 462 return new \WP_Error('invalid_token', __('Invalid verification token. Please try again', 'fluent-community'));
393 463 }
394 464
395 465 if ((int)$data['expires'] < time()) {
466 + delete_transient($transientKey);
396 467 return new \WP_Error('expired_token', __('Verification token has expired. Please try again.', 'fluent-community'));
397 468 }
398 469
399 470 if (!isset($formData['email']) || $data['email'] !== $formData['email']) {
@@ -399,11 +470,22 @@
399 470 if (!isset($formData['email']) || $data['email'] !== $formData['email']) {
400 471 return new \WP_Error('invalid_email', __('Invalid email address. Please try again', 'fluent-community'));
401 472 }
402 473
474 + // Cap online guesses per challenge: after too many wrong codes the challenge is burned.
475 + if ((int) Arr::get($data, 'attempts', 0) >= 10) {
476 + delete_transient($transientKey);
477 + return new \WP_Error('too_many_attempts', __('Too many invalid attempts. Please try again', 'fluent-community'));
478 + }
479 +
403 480 if (!wp_check_password($code, $data['code_hash'])) {
481 + $data['attempts'] = (int) Arr::get($data, 'attempts', 0) + 1;
482 + set_transient($transientKey, $data, max(1, (int) $data['expires'] - time()));
404 483 return new \WP_Error('invalid_code', __('Invalid verification code. Please try again', 'fluent-community'));
405 484 }
485 +
486 + // Single-use: consume the challenge on success.
487 + delete_transient($transientKey);
406 488
407 489 return true;
408 490 }
409 491