Skip to content

feature: 2fa with authenticator apps#1852

Open
sapayth wants to merge 4 commits intoweDevsOfficial:developfrom
sapayth:feature/2fa_with_authenticator_app
Open

feature: 2fa with authenticator apps#1852
sapayth wants to merge 4 commits intoweDevsOfficial:developfrom
sapayth:feature/2fa_with_authenticator_app

Conversation

@sapayth
Copy link
Copy Markdown
Member

@sapayth sapayth commented Apr 29, 2026

closes #1506
closes #1507

Summary

Adds first-class Two-Factor Authentication to WP User Frontend's free edition. Users can secure their accounts with a standards-compliant authenticator app (Google Authenticator, 1Password, Authy, etc.) and are challenged for a 6-digit code on the WPUF login form after their password is verified.

The feature is off by default and is gated behind a new admin settings section (WPUF Settings → 2FA). Once enabled, an authenticated user gets a new Security tab inside [wpuf_account] to enroll, regenerate, or disable their authenticator. Admins get a Reset 2FA action on Users → Edit User for lockout recovery, with an audit trail.

The MVP architecture is multi-method aware (TOTP today; Email OTP / SMS OTP can plug in later via wpuf_2fa_register_methods) but ships only TOTP.

Spec: docs/totp.md.


What's new

User-facing

  • New Security tab in [wpuf_account] (only visible when 2FA is enabled globally).
  • Enrollment flow with QR code, manual entry key, and 6-digit confirmation.
  • Regenerate secret and Disable 2FA actions, both re-prompting for password + current code.
  • Login challenge on the WPUF login form — credentials in step 1, code in step 2; the auth cookie is never set until the code is verified.
  • Friendly error states (That code didn't match., Your sign-in session expired., Too many attempts.) instead of generic WP errors.

Admin-facing

  • New settings section WPUF Settings → 2FA with:
    • Enable Two-Factor Authentication toggle
    • 2FA Methods selector (card grid; TOTP today)
    • App Display Name — issuer label shown inside the user's authenticator app, defaults to the site name
  • New section on Users → Edit User showing enrollment status, recent activity (audit log), and a Reset 2FA button (administrators only, capability manage_options).

Architecture / extensibility

  • New namespace WeDevs\Wpuf\TwoFactor\ (includes/TwoFactor/):
    • Two_Factor_Manager — service container, accessed as wpuf()->two_factor
    • TOTP_Method implements a generic Method_Interface, registered through Method_Registry so future methods slot in without rewriting the challenge controller
    • Enrollment_Controller, Login_Controller, Account_Section, Admin_User_Profile
    • User_Storage (encrypted secret + replay step + audit log + enrollment timestamp)
    • Crypto — AUTH_KEY-derived encryption for the at-rest secret
    • Rate_Limiter — 5 failures → 15-minute soft lock, per user
    • QR_Renderer — server-side QR generation (secrets never leave the site)
  • Generic method_selector settings field type added to Lib/WeDevs_Settings_API.php (extracted from gateway_selector; both share a private render_card_selector() with their own BEM namespaces). Reusable for future selectors (social login, etc.) without further duplication.
  • New extensibility hooks: wpuf_2fa_register_methods, wpuf_2fa_methods (filter), wpuf_2fa_totp_reset (action). Spec lists more hooks reserved for future phases.

Summary by CodeRabbit

  • New Features

    • Two‑Factor Authentication: enroll with authenticator apps (QR + manual key), verify codes, manage multiple methods, and use 2FA at login.
    • New Security dashboard tab to enroll/manage 2FA and view recent activity.
    • Admin panel to view/reset a user’s 2FA.
  • Improvements

    • Unified card-style selector UI for methods/gateways and dedicated 2FA styles/scripts for login and account flows.
    • Minimum PHP requirement raised to 7.4.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Walkthrough

Adds a complete Two‑Factor Authentication subsystem (TOTP method, QR rendering, secret encryption, per‑user storage, rate limiting, enrollment/login controllers, registry/interface, admin/account UI and assets), generalizes settings card selector for gateways/methods, updates assets/styles/scripts, raises minimum PHP to 7.4, and wires the 2FA manager into plugin bootstrap.

Changes

2FA Core & Storage

Layer / File(s) Summary
Bootstrap / Wiring
includes/TwoFactor/Two_Factor_Manager.php
New manager constructs crypto, storage, rate limiter, QR renderer and method registry, registers TOTP method, exposes hooks to register additional methods, and registers admin integrations.
Crypto
includes/TwoFactor/Crypto.php
Adds AES‑256‑GCM encrypt/decrypt with v1 payload format and HKDF key derivation.
User Storage
includes/TwoFactor/User_Storage.php
Per‑user method secret persistence (encrypt/decrypt), pending enrollment transients, issued code transient handling, method state APIs, and audit log append/get.
Rate Limiting
includes/TwoFactor/Rate_Limiter.php
Per‑user failure counter, lockout expiry, record/clear failure APIs.

TOTP Method & QR

Layer / File(s) Summary
TOTP Implementation
includes/TwoFactor/TOTP_Method.php
New TOTP method implementing Method_Interface: start/confirm enrollment, issue challenge, verify with replay protection/step tracking, reset, and uses rate limiter and storage.
QR Renderer
includes/TwoFactor/QR_Renderer.php
Server SVG QR generation using BaconQrCode with graceful empty-string fallback on errors.
Composer deps
composer.json
Adds pragmarx/google2fa and bacon/bacon-qr-code and raises PHP requirement to >=7.4.

Method Interface & Registry

Layer / File(s) Summary
Interface
includes/TwoFactor/Method_Interface.php
New interface defining IDs, labels, enrollment lifecycle, challenge issuance/verification, and reset.
Registry
includes/TwoFactor/Method_Registry.php
Registry to register/enumerate methods, return active methods (from option), and determine per‑user enrolled methods.

Enrollment AJAX & Login Flow

Layer / File(s) Summary
AJAX Controller
includes/TwoFactor/Enrollment_Controller.php
Adds wp_ajax endpoints (start/confirm/issue/disable) with nonce checks, resolves method by id, forwards sanitized input, formats WP_Error JSON responses, manages enrollment/disable flows and audit events.
Login Controller
includes/TwoFactor/Login_Controller.php
Implements multi‑stage login: authenticate filter to abort initial login and create selector/challenge tokens, init handlers to handle selector and challenge posts, transients for tokens, issuing challenges, verification, and rendering injected 2FA UI in login form.

Account & Admin UI Integration

Layer / File(s) Summary
Account Section
includes/TwoFactor/Account_Section.php, templates/dashboard/security.php
Adds dashboard “Security” tab wiring, enqueues assets, and template that renders filtered per‑method card HTML with nonce/ajax attributes.
Default Card Renderer
includes/TwoFactor/Default_Card_Renderer.php
Filter consumer producing enrollment UI (QR, secret, verify) and enrolled UI (destination, disable flows) when template HTML not provided.
Admin User Profile
includes/TwoFactor/Admin_User_Profile.php
Admin Users → Edit screen panel showing enrolled methods, admin reset handler (nonce + capability), audit logging, and redirect on reset.
Client JS (Account)
assets/js/wpuf-2fa-account.js
Front‑end controller for account security: AJAX start/confirm/issue/disable flows, QR injection, message UI, button enable/disable, and error handling.
2FA Login Assets
assets/css/wpuf-2fa-login.css, assets/js/wpuf-2fa-login.js
Styles to hide credential UI during 2FA stages and JS to gate method‑picker radio submission until real interaction.

Settings, Card Selector & Template Hooks

Layer / File(s) Summary
Settings API refactor
Lib/WeDevs_Settings_API.php
Generalizes gateway selector by adding callback_method_selector() and delegating both gateway and method selectors to render_card_selector( $args, $config ) producing BEM namespaced markup wpuf-gateway-card/wpuf-method-card.
Settings fields
includes/functions/settings-options.php, wpuf-functions.php
Adds wpuf_2fa settings (enable toggle, method selector, TOTP issuer), and new helper wpuf_get_2fa_methods() to enumerate card metadata for settings.
Login template & hooks
templates/login-form.php, includes/Free/Simple_Login.php
Login template gains filterable text variables and action hooks for error/message rendering; Simple_Login refactored to use filterable Turnstile verification pipeline and avoids extra validation during 2FA stage‑2.
Dashboard template & card filter
templates/dashboard/security.php, includes/TwoFactor/Default_Card_Renderer.php
Template renders per‑method sanitized card HTML via wpuf_2fa_security_card_html filter.
Settings JS/CSS adjustments
assets/js/admin/settings.js, assets/css/admin.css, assets/less/admin.less
Adds method selector JS init, duplicates/extends card CSS to .wpuf-method-* BEM selectors, and minor formatting changes in settings JS dependency lookups.

Assets Registration & Bootstrap

Layer / File(s) Summary
Asset registration
includes/Assets.php
Registers new style/script handles: wpuf-2fa-account, wpuf-2fa-login, and wpuf-2fa-login.css.
Plugin bootstrap
wpuf.php
Bumps plugin min PHP to 7.4, adjusts version check comparison behavior, and instantiates Two_Factor_Manager into the container.
PHPCS / readme
phpcs.xml.dist, readme.txt
Adjusts PHPCS baseline to PHP 7.4 and updates README PHP requirement.
Composer / deps
composer.json
Sets prefer‑stable, updates PHP platform to >=7.4, adds QR/TOTP packages.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

needs: dev review

Poem

🐰
I nibbled keys and hid the trace,
Sketched QR hops with hoppity grace,
Cards now share a tidy name,
TOTP guards the login game,
A carrot, locked and encrypted — safe place.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR objectives reference linked issues #1506 and #1507 about form builder enhancements, but the changeset exclusively implements 2FA functionality with no form builder, field icons, or settings UI revamp changes. Review linked issues; this PR appears to address a different feature (2FA) than the form builder enhancements listed in #1506 and #1507.
Docstring Coverage ⚠️ Warning Docstring coverage is 65.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title "feature: 2fa with authenticator apps" clearly and concisely summarizes the main change: adding Two-Factor Authentication with authenticator app support.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing 2FA functionality (TOTP method, enrollment, login integration, admin UI, encryption, storage, rate limiting, QR rendering, and dependencies). No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.51)

PHPStan was skipped because the sandbox runner could not parse its output.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
Lib/WeDevs_Settings_API.php (1)

1050-1065: Deduplicate dependency-field lookup to prevent drift.

The same fallback selector chain is repeated twice; extracting it into one helper inside script() will reduce maintenance bugs.

Refactor sketch
+                function findDependencyField(field_name) {
+                    var $dep = $("input[id*='"+ field_name +"'], select[id*='"+ field_name +"']");
+                    if ($dep.length === 0) $dep = $("input[name*='["+ field_name +"]'], select[name*='["+ field_name +"]']");
+                    if ($dep.length === 0) $dep = $("input[id*='["+ field_name +"]'], select[id*='["+ field_name +"]']");
+                    if ($dep.length === 0) $dep = $("input[id*='"+ field_name +"'], select[id*='"+ field_name +"']");
+                    return $dep;
+                }
...
-                        var $depends_on = $("input[id*='"+ field_name +"'], select[id*='"+ field_name +"']");
-                        ...
+                        var $depends_on = findDependencyField(field_name);
...
-                            var $depends_on = $("input[id*='"+ field_name +"'], select[id*='"+ field_name +"']");
-                            ...
+                            var $depends_on = findDependencyField(field_name);

Also applies to: 1125-1140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Lib/WeDevs_Settings_API.php` around lines 1050 - 1065, The repeated fallback
selector chain that builds $depends_on from field_name (the four jQuery lookups
using $("input[id*='"+ field_name +"']" etc.) should be extracted into a single
helper inside script(), e.g. a function like resolveDependencyField(field_name)
that returns the jQuery collection; replace both duplicated blocks (the
occurrences around $depends_on and the second block at lines noted in the
review) with calls to that helper so the lookup logic is centralized and not
duplicated. Ensure the helper reproduces the same selector priority and returns
an empty collection if nothing is found.
includes/functions/settings-options.php (1)

37-41: Consider using a more distinctive icon for the 2FA section.

The dashicons-id icon is already used by the "My Account" section (line 30). Consider using dashicons-lock or dashicons-shield to visually distinguish the 2FA security section.

🎨 Proposed change
         [
             'id'    => 'wpuf_2fa',
             'title' => __( '2FA', 'wp-user-frontend' ),
-            'icon'  => 'dashicons-id',
+            'icon'  => 'dashicons-shield',
         ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/functions/settings-options.php` around lines 37 - 41, The 2FA
section uses the same dashicons-id icon as the My Account section; update the
icon in the settings array for the entry with 'id' => 'wpuf_2fa' to a more
distinctive security glyph (e.g., 'dashicons-lock' or 'dashicons-shield') by
replacing the 'icon' value for that array element so the 2FA/security section is
visually distinct.
includes/TwoFactor/QR_Renderer.php (1)

5-9: Remove unused import.

GDLibRenderer is imported on line 5 but never used in the class. Only ImageRenderer, SvgImageBackEnd, RendererStyle, and Writer are utilized.

🧹 Proposed fix
 namespace WeDevs\Wpuf\TwoFactor;

-use BaconQrCode\Renderer\GDLibRenderer;
 use BaconQrCode\Renderer\ImageRenderer;
 use BaconQrCode\Renderer\Image\SvgImageBackEnd;
 use BaconQrCode\Renderer\RendererStyle\RendererStyle;
 use BaconQrCode\Writer;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/QR_Renderer.php` around lines 5 - 9, Remove the unused
import GDLibRenderer from the top of the QR_Renderer.php file: locate the use
statement "use BaconQrCode\Renderer\GDLibRenderer;" and delete it so only
ImageRenderer, SvgImageBackEnd, RendererStyle, and Writer remain; verify no
other references to GDLibRenderer in the class (e.g., in methods of QR_Renderer)
before committing.
includes/TwoFactor/TOTP_Method.php (1)

24-24: SECRET_LENGTH_BYTES is the wrong unit here.

Google2FA::generateSecretKey() uses a Base32 character count, and the library documents 16 chars as 80 bits and 32 chars as 160 bits. By inference, passing 20 here is about 100 bits of entropy, so both the constant name and the “160 bits” comment are misleading. Either rename this to characters or bump it to 32 if 160-bit secrets are the goal. (github.com)

Suggested fix
-    public const SECRET_LENGTH_BYTES = 20; // 160 bits per RFC 6238 §5.
+    public const SECRET_LENGTH_CHARS = 32; // 160 bits of entropy with Base32 output.
...
-        return $this->google2fa->generateSecretKey( self::SECRET_LENGTH_BYTES );
+        return $this->google2fa->generateSecretKey( self::SECRET_LENGTH_CHARS );

Also applies to: 68-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/TOTP_Method.php` at line 24, The constant
SECRET_LENGTH_BYTES in TwoFactor/TOTP_Method.php is misnamed and misdocumented:
Google2FA expects a Base32 character count, not raw bytes, so either rename
SECRET_LENGTH_BYTES to SECRET_LENGTH_CHARS and keep the value 20 (and update the
comment to reflect ~100 bits of entropy), or if you actually want 160-bit
secrets set the constant to 32 (characters) and update the comment to "32 base32
chars ≈160 bits per RFC 6238 §5"; apply the same rename/value/comment change
wherever the constant is used (also update the occurrences referenced around
lines 68–70). Ensure function calls that pass this constant to
Google2FA::generateSecretKey() reflect the renamed constant name if you rename
it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/js/wpuf-2fa-account.js`:
- Around line 34-43: The failure branch for the ajax action wpuf_2fa_totp_start
writes errors into the hidden container .wpuf-2fa-totp-enrollment (updating
.wpuf-2fa-message) without revealing it, so users see no feedback; modify the
failure handling in the $.post(...).done(...) and the other failure path (lines
around 59-61) to call showMessage on a visible container (or ensure the
enrollment panel is shown first) — e.g., ensure you call
$root.find('.wpuf-2fa-totp-enrollment').show() or target a visible selector
before invoking showMessage so errors from wpuf_2fa_totp_start are visible to
users.

In `@includes/TwoFactor/Crypto.php`:
- Around line 26-44: In the encrypt method (function encrypt) ensure you check
the return value of openssl_encrypt for failure before building the payload: if
openssl_encrypt returns false, throw a clear exception (e.g., RuntimeException)
or return an error indicator instead of continuing to base64_encode a boolean;
update the function to validate $ciphertext !== false, handle/log the error and
only construct and return the STORAGE_VERSION.iv.tag.ciphertext payload when
$ciphertext is valid.

In `@includes/TwoFactor/Login_Controller.php`:
- Around line 119-130: After calling wp_set_auth_cookie($user_id, $remember)
fire the standard WordPress login hook so integrations see the 2FA login:
retrieve the WP_User for $user_id (e.g. via get_userdata($user_id)) and call
do_action('wp_login', $user->user_login, $user) immediately after
wp_set_auth_cookie and before wp_safe_redirect/exit; keep the existing
delete_transient(...) behavior intact.

In `@includes/TwoFactor/TOTP_Method.php`:
- Around line 145-160: The verify_against_secret() method currently returns only
true/false and discards the matched TOTP step; change it to return the matched
step (e.g., integer step) on success (or false/null on failure) so callers can
persist it, then update Enrollment_Controller::ajax_confirm() to capture that
returned step and set META_TOTP_LAST_STEP for the user (instead of activating
the secret without recording the step); keep the existing rate_limiter checks
(is_locked, record_failure, clear) and only return the step after clearing the
rate limiter so the exact confirming code cannot be replayed.

In `@Lib/WeDevs_Settings_API.php`:
- Around line 422-430: In the foreach over $args['options'] (the block iterating
$option), guard access to $option['admin_label'] before reading it: replace the
direct index access with a safe lookup (e.g., isset($option['admin_label']) or
null coalescing) and provide a sensible default when missing; update the
assignment to $admin_label so it never triggers a PHP notice for
incomplete/filtered option entries while preserving existing behavior for
present labels.

In `@wpuf-functions.php`:
- Around line 955-958: Update the docblock so it accurately reflects shipped
defaults by removing or rewording the claim that Email OTP and SMS OTP "ship as
pro previews" and instead state that only TOTP is included in the default
registry (exposed as "totp"); adjust the comment text near the gateway-selector
card description to reference only the functional default method "totp" and note
Email/SMS OTP as future/pro-planned if desired.

In `@wpuf.php`:
- Line 60: The PHP version boundary check is incorrect: the class property
$min_php = '7.4' is declared but the runtime check uses a <= comparison that
rejects 7.4; change the conditional (the check around lines 95–99) to use a
strict less-than comparison (or use version_compare($current_php,
$this->min_php, '<')) so that PHP 7.4 is accepted; update the method performing
this check (referenced by $min_php and the existing <= comparison) accordingly
and ensure any related error message reflects the corrected boundary logic.

---

Nitpick comments:
In `@includes/functions/settings-options.php`:
- Around line 37-41: The 2FA section uses the same dashicons-id icon as the My
Account section; update the icon in the settings array for the entry with 'id'
=> 'wpuf_2fa' to a more distinctive security glyph (e.g., 'dashicons-lock' or
'dashicons-shield') by replacing the 'icon' value for that array element so the
2FA/security section is visually distinct.

In `@includes/TwoFactor/QR_Renderer.php`:
- Around line 5-9: Remove the unused import GDLibRenderer from the top of the
QR_Renderer.php file: locate the use statement "use
BaconQrCode\Renderer\GDLibRenderer;" and delete it so only ImageRenderer,
SvgImageBackEnd, RendererStyle, and Writer remain; verify no other references to
GDLibRenderer in the class (e.g., in methods of QR_Renderer) before committing.

In `@includes/TwoFactor/TOTP_Method.php`:
- Line 24: The constant SECRET_LENGTH_BYTES in TwoFactor/TOTP_Method.php is
misnamed and misdocumented: Google2FA expects a Base32 character count, not raw
bytes, so either rename SECRET_LENGTH_BYTES to SECRET_LENGTH_CHARS and keep the
value 20 (and update the comment to reflect ~100 bits of entropy), or if you
actually want 160-bit secrets set the constant to 32 (characters) and update the
comment to "32 base32 chars ≈160 bits per RFC 6238 §5"; apply the same
rename/value/comment change wherever the constant is used (also update the
occurrences referenced around lines 68–70). Ensure function calls that pass this
constant to Google2FA::generateSecretKey() reflect the renamed constant name if
you rename it.

In `@Lib/WeDevs_Settings_API.php`:
- Around line 1050-1065: The repeated fallback selector chain that builds
$depends_on from field_name (the four jQuery lookups using $("input[id*='"+
field_name +"']" etc.) should be extracted into a single helper inside script(),
e.g. a function like resolveDependencyField(field_name) that returns the jQuery
collection; replace both duplicated blocks (the occurrences around $depends_on
and the second block at lines noted in the review) with calls to that helper so
the lookup logic is centralized and not duplicated. Ensure the helper reproduces
the same selector priority and returns an empty collection if nothing is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2618ed8-4e05-49d4-8e1b-d3802dde1aa9

📥 Commits

Reviewing files that changed from the base of the PR and between 3252a39 and d8ebe4b.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • Lib/WeDevs_Settings_API.php
  • assets/css/admin.css
  • assets/js/admin/settings.js
  • assets/js/wpuf-2fa-account.js
  • assets/less/admin.less
  • composer.json
  • includes/Assets.php
  • includes/Free/Simple_Login.php
  • includes/TwoFactor/Account_Section.php
  • includes/TwoFactor/Admin_User_Profile.php
  • includes/TwoFactor/Crypto.php
  • includes/TwoFactor/Enrollment_Controller.php
  • includes/TwoFactor/Login_Controller.php
  • includes/TwoFactor/Method_Interface.php
  • includes/TwoFactor/Method_Registry.php
  • includes/TwoFactor/QR_Renderer.php
  • includes/TwoFactor/Rate_Limiter.php
  • includes/TwoFactor/TOTP_Method.php
  • includes/TwoFactor/Two_Factor_Manager.php
  • includes/TwoFactor/User_Storage.php
  • includes/functions/settings-options.php
  • phpcs.xml.dist
  • readme.txt
  • templates/dashboard/security.php
  • wpuf-functions.php
  • wpuf.php

Comment thread assets/js/wpuf-2fa-account.js
Comment thread includes/TwoFactor/Crypto.php
Comment thread includes/TwoFactor/Login_Controller.php
Comment thread includes/TwoFactor/TOTP_Method.php Outdated
Comment thread Lib/WeDevs_Settings_API.php
Comment thread wpuf-functions.php
sapayth added 2 commits April 30, 2026 15:49
2FA framework: TOTP authenticator support, multi-method registry,
enrollment / challenge / verify flow, security dashboard section, and
extracted login-stage CSS/JS to dedicated wpuf-2fa-login assets.

Login audit (docs/login-process-audit.md, Phases 1-3):
- Reconciled the two Turnstile settings on `login_form_turnstile`.
- Removed dead Turnstile error string and the `form_align` dead pipeline.
- New filters: `wpuf_login_turnstile_verify`, `wpuf_login_form_layout_class`,
  `wpuf_login_layouts`, `wpuf_login_form_template_args`.
- New actions: `wpuf_login_show_errors`, `wpuf_login_show_messages`.
- Layout class applied server-side (no FOUC); single source of truth
  for the layout list.
- Server-side rendering of customizable labels / placeholders / button /
  remember-me / lost-password / form heading — fixes a11y, page caches,
  and silent shortcode-page heading.
- Fire wp_login action after auth cookie in 2FA stage 2 so integrations
  listening for logins (audit logs, tracking) see successful 2FA logins.
- Guard openssl_encrypt() failure in TwoFactor\Crypto::encrypt() to avoid
  base64-encoding a false return.
- Use strict less-than for the PHP version gate so the declared 7.4
  minimum is actually accepted.
- isset() guard on admin_label in settings API multiselect-card to avoid
  notices for filter-extended option arrays.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
includes/TwoFactor/Login_Controller.php (1)

154-166: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fire wp_login hook to preserve WordPress authentication chain compatibility.

After wp_set_auth_cookie(), the wp_login action is not fired. Core's wp_signon() fires this hook after setting the auth cookie, so integrations and plugins listening for user logins will miss successful 2FA authentications, breaking audit logs, tracking, and conditional logic that depends on this hook.

Suggested fix
         // Success: consume token, set the auth cookie, redirect.
         delete_transient( self::TRANSIENT_KEY . $token );
 
         $remember = ! empty( $challenge['remember'] );
         wp_set_auth_cookie( $user_id, $remember );
 
+        $user = get_userdata( $user_id );
+        if ( $user instanceof \WP_User ) {
+            do_action( 'wp_login', $user->user_login, $user );
+        }
+
         $redirect = ! empty( $challenge['final_redirect'] )
             ? $challenge['final_redirect']
             : home_url( '/' );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/Login_Controller.php` around lines 154 - 166, After
calling wp_set_auth_cookie($user_id, $remember) in Login_Controller, fire the
core wp_login action so other plugins see the successful login: retrieve the
WP_User via get_userdata($user_id) (or wp_get_current_user() if preferred), then
call do_action('wp_login', $user->user_login, $user) immediately after
wp_set_auth_cookie and before wp_safe_redirect/exit; keep the existing
delete_transient(self::TRANSIENT_KEY . $token) behavior intact.
🧹 Nitpick comments (1)
includes/TwoFactor/Login_Controller.php (1)

220-223: 💤 Low value

Apply consistent sanitization to $_POST['rememberme'].

For consistency with coding guidelines, apply wp_unslash() before casting. While the boolean cast makes this safe in practice, consistent sanitization patterns are preferred.

Suggested fix
         // phpcs:ignore WordPress.Security.NonceVerification.Missing
-        $remember = isset( $_POST['rememberme'] ) ? (bool) $_POST['rememberme'] : false;
+        $remember = ! empty( $_POST['rememberme'] );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/Login_Controller.php` around lines 220 - 223, Update the
sanitization for the remember-me input in Login_Controller by applying
wp_unslash() to $_POST['rememberme'] before casting to boolean so $remember is
set consistently (i.e., replace the current direct isset/ cast with an isset
check that uses wp_unslash($_POST['rememberme']) and then (bool) on that value);
keep the existing phpcs ignore comment if needed and mirror the pattern used for
$final_redirect to maintain consistent sanitization style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@includes/TwoFactor/Admin_User_Profile.php`:
- Around line 36-39: Replace the current_user_can('manage_options') check in
render_section with current_user_can(wpuf_admin_role()) and ensure the
corresponding form handler (the admin reset handler around lines 166-176) also
uses current_user_can(wpuf_admin_role()) and check_ajax_referer() for CSRF
protection; additionally, when reading user_id from $_POST in the handler, use
wp_unslash($_POST['user_id']) then absint() to sanitize, and apply similar
wp_unslash()+appropriate sanitization for any other superglobal inputs touched
by the handler.

---

Duplicate comments:
In `@includes/TwoFactor/Login_Controller.php`:
- Around line 154-166: After calling wp_set_auth_cookie($user_id, $remember) in
Login_Controller, fire the core wp_login action so other plugins see the
successful login: retrieve the WP_User via get_userdata($user_id) (or
wp_get_current_user() if preferred), then call do_action('wp_login',
$user->user_login, $user) immediately after wp_set_auth_cookie and before
wp_safe_redirect/exit; keep the existing delete_transient(self::TRANSIENT_KEY .
$token) behavior intact.

---

Nitpick comments:
In `@includes/TwoFactor/Login_Controller.php`:
- Around line 220-223: Update the sanitization for the remember-me input in
Login_Controller by applying wp_unslash() to $_POST['rememberme'] before casting
to boolean so $remember is set consistently (i.e., replace the current direct
isset/ cast with an isset check that uses wp_unslash($_POST['rememberme']) and
then (bool) on that value); keep the existing phpcs ignore comment if needed and
mirror the pattern used for $final_redirect to maintain consistent sanitization
style.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81e5d2f8-2e79-4119-aa0c-b250e5fd4c07

📥 Commits

Reviewing files that changed from the base of the PR and between d8ebe4b and e978864.

📒 Files selected for processing (17)
  • assets/css/wpuf-2fa-login.css
  • assets/js/wpuf-2fa-account.js
  • assets/js/wpuf-2fa-login.js
  • includes/Assets.php
  • includes/Free/Simple_Login.php
  • includes/TwoFactor/Account_Section.php
  • includes/TwoFactor/Admin_User_Profile.php
  • includes/TwoFactor/Default_Card_Renderer.php
  • includes/TwoFactor/Enrollment_Controller.php
  • includes/TwoFactor/Login_Controller.php
  • includes/TwoFactor/Method_Interface.php
  • includes/TwoFactor/TOTP_Method.php
  • includes/TwoFactor/Two_Factor_Manager.php
  • includes/TwoFactor/User_Storage.php
  • templates/dashboard/security.php
  • templates/login-form.php
  • wpuf-functions.php
✅ Files skipped from review due to trivial changes (1)
  • assets/css/wpuf-2fa-login.css
🚧 Files skipped from review as they are similar to previous changes (5)
  • includes/Assets.php
  • includes/TwoFactor/Two_Factor_Manager.php
  • templates/dashboard/security.php
  • includes/TwoFactor/Account_Section.php
  • assets/js/wpuf-2fa-account.js

Comment on lines +36 to +39
public function render_section( $user ) {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align admin reset guardrails with project security policy.

Use wpuf_admin_role() capability checks in both UI + handler, sanitize user_id from $_POST with wp_unslash() + absint(), and switch to check_ajax_referer() for the form handler guard.

🔧 Suggested patch
 public function render_section( $user ) {
-    if ( ! current_user_can( 'manage_options' ) ) {
+    if ( ! current_user_can( wpuf_admin_role() ) ) {
         return;
     }
@@
 public function handle_reset() {
-    $target_user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
+    $target_user_id = isset( $_POST['user_id'] ) ? absint( wp_unslash( $_POST['user_id'] ) ) : 0;
 
-    if ( ! current_user_can( 'manage_options' ) ) {
+    if ( ! current_user_can( wpuf_admin_role() ) ) {
         wp_die( esc_html__( 'You do not have permission to reset 2FA for this user.', 'wp-user-frontend' ), 403 );
     }
 
-    $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ) : '';
-
-    if ( ! wp_verify_nonce( $nonce, self::ACTION . '_' . $target_user_id ) ) {
+    if ( ! check_ajax_referer( self::ACTION . '_' . $target_user_id, '_wpnonce', false ) ) {
         wp_die( esc_html__( 'Security check failed.', 'wp-user-frontend' ), 403 );
     }

As per coding guidelines, "Include check_ajax_referer() and current_user_can(wpuf_admin_role()) permission checks in every form and AJAX handler" and "Sanitize all superglobal access ($_POST, $_GET, $_REQUEST, $_SERVER) with wp_unslash() and appropriate sanitization functions".

Also applies to: 166-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/Admin_User_Profile.php` around lines 36 - 39, Replace the
current_user_can('manage_options') check in render_section with
current_user_can(wpuf_admin_role()) and ensure the corresponding form handler
(the admin reset handler around lines 166-176) also uses
current_user_can(wpuf_admin_role()) and check_ajax_referer() for CSRF
protection; additionally, when reading user_id from $_POST in the handler, use
wp_unslash($_POST['user_id']) then absint() to sanitize, and apply similar
wp_unslash()+appropriate sanitization for any other superglobal inputs touched
by the handler.

@weDevsOfficial weDevsOfficial deleted a comment from coderabbitai Bot May 4, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@includes/TwoFactor/Login_Controller.php`:
- Around line 307-316: The code treats any \WP_Error from
issue_and_mint_challenge() as a failure and always calls push_login_error(), but
issue_and_mint_challenge() returns a WP_Error with code 'wpuf_2fa_required' on
success; update the guard to only call push_login_error() for the genuine
failure code (e.g. check is_wp_error($result) && $result->get_error_code() ===
'wpuf_2fa_issue_failed') so the challenge prompt returned as 'wpuf_2fa_required'
is not rendered as an error; locate references to issue_and_mint_challenge and
push_login_error in Login_Controller and adjust the conditional accordingly.
- Around line 258-261: Simple_Login::process_login currently only early-returns
for wpuf_2fa_token, so submissions from the stage-1.5 method-choice form
(wpuf_2fa_selector_token) fall through and trigger a "Username is required"
error; update the early-return check in process_login to also detect
wpuf_2fa_selector_token (e.g., change the existing if that checks wpuf_2fa_token
to also check wpuf_2fa_selector_token or add a separate early-return) so the
handler returns immediately when either token is present and does not attempt to
extract $_POST['log'] or $_POST['pwd'].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8bf2f58d-0159-47c9-8a9a-e43a4d95e53d

📥 Commits

Reviewing files that changed from the base of the PR and between e978864 and 0eebe34.

📒 Files selected for processing (4)
  • Lib/WeDevs_Settings_API.php
  • includes/TwoFactor/Crypto.php
  • includes/TwoFactor/Login_Controller.php
  • wpuf.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • wpuf.php
  • Lib/WeDevs_Settings_API.php
  • includes/TwoFactor/Crypto.php

Comment on lines +258 to +261
public function maybe_handle_method_choice() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $_POST['wpuf_2fa_selector_token'] ) || empty( $_POST['wpuf_login'] ) ) {
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Check Simple_Login::process_login() for any wpuf_2fa_selector_token guard
echo "=== Simple_Login early-return guards ==="
rg -n "wpuf_2fa_selector_token|2fa_selector" --type php

# 2. Check whether the login form template strips (not just hides) credential fields
echo ""
echo "=== Login template 2FA field handling ==="
fd -e php -g '*login*' | xargs grep -l "2fa" 2>/dev/null
fd -e php -g '*login*' | xargs grep -n "wpuf_2fa_selector\|wpuf_login\|username\|password" 2>/dev/null | head -60

# 3. Confirm JS approach (hide vs remove credential fields)
echo ""
echo "=== 2FA login JS: field hiding strategy ==="
fd -e js -g '*2fa*' | xargs cat 2>/dev/null | head -80

Repository: weDevsOfficial/wp-user-frontend

Length of output: 11321


🏁 Script executed:

#!/bin/bash
# Get the full process_login() method implementation
echo "=== Simple_Login::process_login() full method ==="
sed -n '594,750p' includes/Free/Simple_Login.php

echo ""
echo "=== Check for login form template handling ==="
# Look for template files
fd -e php -g '*form*' -o -g '*template*' | grep -i login | head -20

Repository: weDevsOfficial/wp-user-frontend

Length of output: 6636


🏁 Script executed:

#!/bin/bash
# Check what happens after line 594 in process_login
echo "=== View context around process_login (lines 590-720) ==="
sed -n '590,720p' includes/Free/Simple_Login.php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 5368


🏁 Script executed:

#!/bin/bash
# View the authenticate filter implementation in Login_Controller
echo "=== Login_Controller authenticate filter (line 195 context) ==="
sed -n '190,210p' includes/TwoFactor/Login_Controller.php

echo ""
echo "=== Check hook registration ==="
rg -n "add_filter.*authenticate" includes/TwoFactor/Login_Controller.php

echo ""
echo "=== Verify stage 1.5 form structure ==="
sed -n '570,600p' includes/TwoFactor/Login_Controller.php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 2520


Add early-return guard for wpuf_2fa_selector_token in Simple_Login::process_login().

Simple_Login::process_login() early-returns only for wpuf_2fa_token (stage 2) but not for wpuf_2fa_selector_token (stage 1.5). When stage 1.5 submissions (method choice) POST to this handler, they bypass the early-return and proceed to extract credentials. Since the stage 1.5 form contains only the selector token and method buttons—not username/password fields—both $_POST['log'] and $_POST['pwd'] are absent, defaulting to empty strings. The handler then exits at lines 650–651 with a spurious "Username is required" error, which displays alongside the 2FA code input.

Add an early-return guard for wpuf_2fa_selector_token immediately after the existing wpuf_2fa_token check (around line 605):

if ( ! empty( $_POST['wpuf_2fa_token'] ) || ! empty( $_POST['wpuf_2fa_selector_token'] ) ) {
    return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/Login_Controller.php` around lines 258 - 261,
Simple_Login::process_login currently only early-returns for wpuf_2fa_token, so
submissions from the stage-1.5 method-choice form (wpuf_2fa_selector_token) fall
through and trigger a "Username is required" error; update the early-return
check in process_login to also detect wpuf_2fa_selector_token (e.g., change the
existing if that checks wpuf_2fa_token to also check wpuf_2fa_selector_token or
add a separate early-return) so the handler returns immediately when either
token is present and does not attempt to extract $_POST['log'] or $_POST['pwd'].

Comment on lines +307 to +316
$result = $this->issue_and_mint_challenge(
$user_id,
$method,
! empty( $selection['remember'] ),
! empty( $selection['final_redirect'] ) ? $selection['final_redirect'] : home_url( '/' )
);

if ( is_wp_error( $result ) ) {
$this->push_login_error( $result->get_error_message() );
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

is_wp_error( $result ) is always true — challenge prompt shows as error styling on stage 1.5 success.

issue_and_mint_challenge() is typed @return \WP_Error and unconditionally returns one: 'wpuf_2fa_issue_failed' on failure, 'wpuf_2fa_required' on success. So the if ( is_wp_error( $result ) ) guard at line 314 always executes, pushing the challenge-prompt message ("Enter the 6-digit code from your authenticator app…") through push_login_error() on the success path. That message then renders inside <div class="wpuf-error"> (per show_errors() in Simple_Login), giving an instructional string the visual treatment of an error.

Narrow the guard to the genuine failure code so the prompt is only pushed on send failure:

🐛 Proposed fix
-        if ( is_wp_error( $result ) ) {
+        if ( is_wp_error( $result ) && $result->get_error_code() === 'wpuf_2fa_issue_failed' ) {
             $this->push_login_error( $result->get_error_message() );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/TwoFactor/Login_Controller.php` around lines 307 - 316, The code
treats any \WP_Error from issue_and_mint_challenge() as a failure and always
calls push_login_error(), but issue_and_mint_challenge() returns a WP_Error with
code 'wpuf_2fa_required' on success; update the guard to only call
push_login_error() for the genuine failure code (e.g. check is_wp_error($result)
&& $result->get_error_code() === 'wpuf_2fa_issue_failed') so the challenge
prompt returned as 'wpuf_2fa_required' is not rendered as an error; locate
references to issue_and_mint_challenge and push_login_error in Login_Controller
and adjust the conditional accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant