diff --git a/changelog.txt b/changelog.txt
index 0c678d8..67c38b2 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,25 @@
== Changelog ==
+= 1.6.7 =
+
+_Release date: 2026-09-17_
+
+**Fixed**
+
+* The forced-setup wizard felt like a broken redirect loop: the wizard redirect bounced users to the profile with no explanation (it now carries a flag that renders a clear "activate at least one verification method, then save" warning), and saving with an expired form nonce silently did nothing (it now raises a visible "form session expired" error so users know to try again).
+
+**Compatibility**
+
+* WordPress: 5.6 – 7.1
+* PHP: 8.0 – 8.5
+
+**Tests**
+
+* PHP Coding Standards: PHP_CodeSniffer 3.13.6 / WPCS 3.4.1 — 0 errors
+* PHPStan: level 9 — 0 errors
+* PHPCompatibility: 8.0–8.5 — 0 issues
+* PHPUnit: 9.6.36 — 98 tests, 197 assertions
+
= 1.6.6 =
_Release date: 2026-09-17_
diff --git a/includes/user/class-grace-period.php b/includes/user/class-grace-period.php
index b074a79..31399d6 100644
--- a/includes/user/class-grace-period.php
+++ b/includes/user/class-grace-period.php
@@ -41,6 +41,7 @@ class Grace_Period {
public function register_hooks(): void {
add_action( 'admin_init', array( $this, 'maybe_redirect_to_setup_wizard' ) );
add_action( 'robotstxt_2fa_method_enabled', array( $this, 'clear_pending_setup' ), 10, 2 );
+ add_action( 'admin_notices', array( $this, 'maybe_show_setup_required_notice' ) );
}
/**
@@ -167,10 +168,45 @@ class Grace_Period {
return;
}
- wp_safe_redirect( admin_url( 'profile.php#robotstxt-2fa-settings' ) );
+ // The query flag lets the profile show WHY the user was redirected,
+ // so the mandatory setup does not feel like a silent loop.
+ $profile_url = add_query_arg( 'robotstxt-2fa-setup-required', '1', admin_url( 'profile.php' ) );
+
+ wp_safe_redirect( $profile_url . '#robotstxt-2fa-settings' );
exit;
}
+ /**
+ * Explain the setup wizard redirect on the profile screen.
+ *
+ * Rendered after {@see self::maybe_redirect_to_setup_wizard()} bounced the
+ * user back to their profile. Without it, users land on a regular profile
+ * page with no hint that 2FA enrollment is mandatory before they can
+ * continue to the dashboard — which reads as a broken redirect loop.
+ *
+ * @since 1.6.7
+ *
+ * @return void
+ */
+ public function maybe_show_setup_required_notice(): void {
+ if ( ! $this->is_pending_setup() ) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only flag; no state change.
+ if ( ! isset( $_GET['robotstxt-2fa-setup-required'] ) ) {
+ return;
+ }
+ ?>
+
+ focus_section = true;
+ add_settings_error(
+ 'robotstxt-2fa',
+ 'robotstxt-2fa-nonce-expired',
+ __( 'Your form session expired and nothing was saved. Please try saving again.', 'robotstxt-2fa' ),
+ 'error'
+ );
+
return;
}
diff --git a/readme.txt b/readme.txt
index c42cca4..fd801dc 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: security, two-factor authentication, login, otp
Requires at least: 5.6
Tested up to: 7.0
Requires PHP: 8.0
-Stable tag: 1.6.6
+Stable tag: 1.6.7
License: GPLv3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -59,6 +59,14 @@ Yes. Activate the plugin at the network level. Network administrators can set an
== Changelog ==
+= 1.6.7 =
+
+_Release date: 2026-09-17_
+
+**Fixed**
+
+* The forced-setup wizard felt like a broken redirect loop: the wizard redirect bounced users to the profile with no explanation (it now carries a flag that renders a clear "activate at least one verification method, then save" warning), and saving with an expired form nonce silently did nothing (it now raises a visible "form session expired" error so users know to try again).
+
= 1.6.6 =
_Release date: 2026-09-17_
@@ -76,14 +84,6 @@ _Release date: 2026-09-17_
* Fatal error on the login screen with newer WordPress versions: the `login_message` filter can deliver null when no message is set (observed on WordPress 7.x with PHP 8.4, TypeError on a plain visit to wp-login.php). The message callback now accepts `string|null` and coalesces null to an empty string.
* Hardened all externally-fed filter callbacks against null payloads: the four `authenticate` callbacks now accept `string|null` credentials (custom REST/SSO endpoints are known to apply the filter with null), and the `wp_redirect` filter callback coalesces a null location.
-= 1.6.4 =
-
-_Release date: 2026-09-11_
-
-**Fixed**
-
-* Compatibility with Restrict Content Pro's "Hijack Login URL" option: RCP's `login_url` filter made every `wp_login_url()` call return a membership page, so the 2FA verification redirect landed on a restricted page where the verification form cannot render, and the visitor was bounced to the registration page. Verification-stage URLs are now built from the canonical `wp-login.php`, mirroring WordPress core's URL construction before the filterable output. The "Back to login" link keeps the site-configured login URL.
-
= Previous versions =
For the full changelog see the [changelog](https://www.robotstxt.software/plugins/robotstxt-2fa/) page.
diff --git a/robotstxt-2fa.php b/robotstxt-2fa.php
index 8ff56f1..1fb6b43 100644
--- a/robotstxt-2fa.php
+++ b/robotstxt-2fa.php
@@ -4,7 +4,7 @@
* Plugin URI: https://www.robotstxt.software/plugins/robotstxt-2fa/
* Update URI: https://www.robotstxt.software/plugins/robotstxt-2fa/
* Description: Adds two-factor authentication to the WordPress login flow.
- * Version: 1.6.6
+ * Version: 1.6.7
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.software/
* Text Domain: robotstxt-2fa
@@ -25,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
if ( ! defined( 'ROBOTSTXT_2FA_VERSION' ) ) {
- define( 'ROBOTSTXT_2FA_VERSION', '1.6.6' );
+ define( 'ROBOTSTXT_2FA_VERSION', '1.6.7' );
}
if ( ! defined( 'ROBOTSTXT_2FA_FILE' ) ) {
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index 7328496..3b5c785 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'robotstxt/robotstxt-2fa',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
- 'reference' => 'f1d493aefc9da797d99b3b635736b6c627166ddf',
+ 'reference' => '2b6e97d03ffbc5f7734bbd30413c979f50de3d15',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -40,7 +40,7 @@
'robotstxt/robotstxt-2fa' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
- 'reference' => 'f1d493aefc9da797d99b3b635736b6c627166ddf',
+ 'reference' => '2b6e97d03ffbc5f7734bbd30413c979f50de3d15',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),