Skip to content

Captcha on login and password reset forms (login hooks + single captcha setting) - #3765

Draft
dparker1005 wants to merge 2 commits into
strangerstudios:v3.9from
dparker1005:enhancement/login-captcha-hooks
Draft

Captcha on login and password reset forms (login hooks + single captcha setting)#3765
dparker1005 wants to merge 2 commits into
strangerstudios:v3.9from
dparker1005:enhancement/login-captcha-hooks

Conversation

@dparker1005

Copy link
Copy Markdown
Member

Phases 1 and 2 of the captcha-on-login work discussed for 3.9 (replaces the approach in #3493; phase 3 is the spam activity table in #3640).

Phase 1: Generic login flow hooks (commit 1, includes/login.php only)

Gives captchas (and anything else) clean hook points in the login flow, mirroring how checkout isolates captcha code:

  • pmpro_login_form_before_submit_button — fires inside the PMPro login form, via a login_form_middle filter added/removed around wp_login_form() (same pattern as the existing hidden-field filter), so it only ever runs on PMPro's forms.
  • pmpro_authenticate_login_checks — filter on authenticate at priority 40, gated to PMPro-form submissions (pmpro_login_form_used) or real wp-login.php login submissions (did_action( 'login_form_login' )). XML-RPC, other plugins' login forms, and programmatic wp_signon() calls are never touched, so a captcha can't lock out a flow that didn't display it.
  • pmpro_login_forms_handler_message / pmpro_login_forms_handler_msgt — lets custom error codes passed back to the login page display messages.
  • Cleanup: pmpro_authenticate_username_password() now only redirects for empty_username/empty_password (the codes WP core never fires wp_login_failed for) and exits after redirecting. All other failures flow through pmpro_login_failed() — previously both functions called wp_redirect() in the same request and the second one happened to win.

Phase 2: Captchas use the hooks (commit 2)

  • Single "Captcha" setting (No / Google reCAPTCHA / Cloudflare Turnstile) on the Security Settings page replaces the two independent on/off settings, so only one captcha runs at a time. Back compat: the old options are read when the new setting has never been saved (a saved "No" does not fall back), and the old options are kept in sync on save for anything that reads them (e.g. Wisdom telemetry).
  • Captcha service registry: new pmpro_captcha_services filter in includes/captcha.php. reCAPTCHA and Turnstile register themselves from their own files, and a future captcha add on can drop in the same way with no core changes. If a registered service disappears (add on deactivated), the site fails safe to no captcha.
  • Captcha on login and lost password forms (PMPro pages + wp-login.php), shown only after a failed login attempt from the visitor's IP and only when keys are configured — so a misconfigured captcha can't lock admins out. The failed-attempt signal is the existing spam activity API, which phase 3 (Spam protection: race condition in transient-based tracking allows brute force bypass #3640) will re-point at the new table with no changes here.
  • Validation is fully server-side. reCAPTCHA v2 and Turnstile need no custom JS on login forms; v3-invisible uses a small inline script that fails open to server-side rejection if Google's script doesn't load. The checkout JS files are untouched.
  • Failed logins are now tracked when a captcha is enabled even if Spam Protection is off.
  • Bug fix found along the way: Turnstile checkout validation set the "validated" session var even when verification failed, so a failed captcha was bypassed on retry. Now only successful validations are remembered.

Testing done

Full curl-based pass on a local site: hook plumbing proven with a throwaway consumer; login/logout/lost password/reset password behavior verified unchanged with captcha off; with dummy keys, challenge lifecycle verified for both services on all four forms (no captcha before a failure, widget after, correct-password-but-no-token rejected with a displayed error, lost password blocked without sending email); checkout gating verified for both services incl. legacy-option derivation; registry verified with a fake third-party service (registers, saves, fails safe when removed).

Still needs a browser pass with real keys: actual token round trips (only the empty-token rejection path is testable with dummy keys), the v3-invisible inline JS, and the settings show/hide toggling.

🤖 Generated with Claude Code

dparker1005 and others added 2 commits August 21, 2026 08:56
- New pmpro_login_form_before_submit_button action inside the PMPro login
  form via a temporary login_form_middle filter.
- New pmpro_authenticate_login_checks filter on authenticate (priority 40),
  scoped to PMPro login form and wp-login.php submissions only so other
  login flows (XML-RPC, other plugins' forms, wp_signon calls) are never
  affected.
- New pmpro_login_forms_handler_message/msgt filters so custom error codes
  passed back to the login page can display messages.
- pmpro_authenticate_username_password() now only redirects for the
  empty_username/empty_password codes that WP core never fires
  wp_login_failed for, and exits after redirecting. All other failures
  redirect through pmpro_login_failed() as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etting

- New Captcha setting on the Security Settings page replaces the separate
  "Use reCAPTCHA?" and "Use CloudFlare Turnstile?" settings. Legacy options
  are still read when the new setting has never been saved and are kept in
  sync on save for backwards compatibility.
- New includes/captcha.php with shared captcha service code: a
  pmpro_captcha_services registry filter so captcha services (including
  future add ons) register themselves from their own files, pmpro_captcha()
  to get the active service, and the shared failed-attempt gate that
  phase 3 can repoint at the spam activity table.
- reCAPTCHA and Turnstile now show on the PMPro and wp-login.php login and
  lost password forms, but only after a failed login attempt is tracked for
  the visitor IP (and only when keys are configured), preventing lockouts
  from misconfigured captchas.
- Failed logins are now tracked when a captcha is enabled, even if spam
  protection is off.
- Fixed Turnstile checkout validation marking the session as validated even
  when verification failed, which let a failed captcha be bypassed on retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant