Releases: radishconcepts/radish-wp-2fa
Release list
v0.2.3 — Fix false "session expired" after successful 2FA
Highlights
Fixes a false negative that made successful sign-ins look failed: users saw "This link has expired" after entering a correct verification code, even though the login had gone through (visiting wp-admin manually proved it).
complete_login() deleted the nonce, so any repeat of the request that finished the flow found no nonce and was indistinguishable from an expired link. In practice that repeat is everyday behaviour: a double-clicked Sign in button, the browser re-sending the POST after back or reload, a restored tab, a retried slow request. Recommended for all installs.
Fixed
- A completed verification now leaves a short-lived receipt behind (
Nonce::complete(), 15 minutes,user_id+ host-validatedredirect_to, no secrets, key still SHA-256-hashed), so a repeated request resolves to the intended destination instead of reporting an expired session. Answering the repeated POST with a redirect also stops the browser from resubmitting it. - Opening a stale 2FA link while the session is still valid redirects onward instead of rendering the expired screen. Users who require 2FA but aren't enrolled land back on
/2fa/setupwith a fresh nonce viaAuth\Enforcement. The 410 expired page is now reserved for requests with genuinely nothing to resume. - The 5-minute nonce TTL ran absolutely from the moment the password was accepted, so waiting for an e-mail code or fetching a phone could expire the flow mid-way. It is now a 5-minute idle window, restarted on every 2FA page view (
Nonce::touch()) and capped by a hard 30-minute ceiling.
Internal
- New
RedirectTarget::after_stale_token()holds the decision as pure logic: receipt → original target, live session → onward, otherwise → sign in again. Nonce::update()preservescreated_atso payload changes mid-flow don't reset the absolute ceiling.
Tests
87 tests, 159 assertions. NonceTest covers the completion receipt (nonce consumed, token still recognisable, receipt carries no secrets, key stays hashed) and the idle window (restart, clamp to ceiling, drop past ceiling, no-op on unknown tokens). RedirectTargetTest covers every branch of after_stale_token().
i18n
No new or changed user-facing strings, so radish-2fa.pot and the Dutch catalog are unchanged.
Full Changelog: v0.2.2...v0.2.3
v0.2.1 — Fix profile method-switch button
Highlights
Fixes the self-service Change method and Reset 2FA buttons on the user profile screen, which silently no-op'd because they were rendered as <form> elements nested inside WordPress's profile form (HTML strips inner forms, so clicks fell through to the outer profile form).
Method switching is now an inline radio-button row in the profile form-table. Picking a different method and saving the profile queues the change; it only takes effect at the user's next sign-in, so the current session never breaks mid-flow. Recommended for all installs running 0.2.0.
Changed
- Self-service method switch is a radio-button row inside the form-table, saved with the regular Update profile button.
- The change is queued via a new
_radish_2fa_pending_methoduser-meta and applied onwp_loginpriority 5 (ahead ofLoginInterceptor::redirect_to_2faatPHP_INT_MAX), which clears the old enrollment so the user lands on/2fa/setupinstead of the old method's challenge. - An info notice on the profile screen surfaces a queued change so the user knows what to expect at next sign-in.
Fixed
- Change method / Reset 2FA buttons no longer silently no-op (form-nesting bug).
- The change-method flow is now part of the profile form itself; Reset 2FA is a nonce-protected admin-post link.
i18n
- Dutch (
nl_NL) translations added for the new self-service strings;radish-2fa.potregenerated and.morecompiled. 101 entries, 0 untranslated.
Full Changelog: v0.2.0...v0.2.1
v0.2.0 — Email-based 2FA
Highlights
Email-based two-factor authentication is now available as an opt-in alternative to TOTP. Existing TOTP users are unaffected; admins must explicitly enable email under Settings → Two-Factor → Authentication methods before users can choose it.
Added
- Email 2FA method — 6-digit numeric OTP, 10-minute TTL, bcrypt-hashed at rest. HTML + plain-text multipart email; subject and body filterable via
radish_2fa_email_subject,radish_2fa_email_html, andradish_2fa_email_alt_body. - Setup chooser — when more than one method is enabled, users land on a chooser before enrolling.
- Rate limiting — 30s cooldown between OTP sends and max 5 sends per rolling hour per user; multisite-safe (site transients).
- Self-service profile section — status, current method, enrolled-at, last-used and remaining backup codes, with Change method and Reset 2FA buttons.
- Theme-overridable templates —
setup-method-chooser.php,setup-email.php,challenge-email.php. wp radish-2fa statusnow reports the active method.- Dutch translations for all new strings;
.potregenerated.
Changed
- Disabling a previously enabled method now destroys the sessions of users currently enrolled in it, forcing them through the setup chooser on the next request.
- The setup nonce no longer pre-generates a TOTP secret. The secret is created only after the user picks TOTP, so abandoning halfway never wastes a secret.
is_enrolled()is method-aware (TOTP requires a stored secret; email enrolment is sufficient on its own) with a back-compat path for users from pre-method releases.
Full changelog: v0.1.2...v0.2.0
v0.1.2 — Fix redirect loop on /2fa/setup
Bugfix release. Recommended for all installs.
Fixed
- Resolve
ERR_TOO_MANY_REDIRECTSon/2fa/setup/for users with an active session at activation time.Auth\Enforcementnow hooks ontemplate_redirect(priority 11) instead ofinit(priority 999); oninitthe skip-on-2FA-pages guardget_query_var( Routes::QUERY_VAR )always returned empty (parse_query had not run yet), so every request created a fresh nonce and redirected again.
Upgrade notes: clear opcache and any HTTP cache (Cloudflare, Varnish, page cache) after deploying — the previous loop response may be cached.
Full Changelog: v0.1.1...v0.1.2
v0.1.1 — Dependency refresh
Maintenance release. No user-facing changes.
Changed
- Bump `pragmarx/google2fa` requirement from `^8.0` to `^9.0`. The library's default secret-key length changed from 16 to 32 characters; this plugin already passed `32` explicitly to `generateSecretKey()`, so behavior is unchanged.
- Bump `actions/checkout` from v4 to v6 and `actions/cache` from v4 to v5 in the CI workflow (Node.js 24 runtime).
Full Changelog: v0.1.0...v0.1.1
v0.1.0 — Initial release
First public release of Radish 2FA — enforceable, frontend-first two-factor authentication for WordPress with role-based hard enforcement.
Added
- Per-role two-factor authentication enforcement with no skip option.
- Frontend setup and challenge flow on
/2fa/setupand/2fa/challenge. - 10 single-use backup codes, bcrypt-hashed at rest.
- TOTP secrets encrypted at rest with
sodium_crypto_secretbox(key derived fromAUTH_KEY+SECURE_AUTH_KEYvia HKDF-SHA256). - Multisite-ready settings and per-user metadata.
- REST and XML-RPC password-login protection for 2FA users; Application Passwords supported.
- Lockout recovery via
RADISH_2FA_DISABLE_FOR_USER_IDconstant, thewp radish-2faWP-CLI command, or the wp-admin user-edit screen. - Theme-overridable templates for setup, challenge, backup codes, and expired-token pages.
radish_2fa_totp_issuerfilter for customizing the TOTP app issuer label.- Dutch translation;
.potfile shipped for additional locales.
See CHANGELOG.md for the full history.