From 2cb923346b29123d62ea746da4c14d90657b55b4 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Fri, 3 Jul 2026 00:31:31 -0600 Subject: [PATCH 1/3] fix: reword mixed-audience login failure notice to be informational MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous notice used "WARNING:" and "If this wasn't you, you should reset your password." The reader has already entered the correct password, so the "if this wasn't you" framing is disorienting for the legitimate user (who likely mistyped a code) and the "reset your password" advice is wrong — the threat is to the second factor, not the password. New text is factual and defers action to after a successful login. Fixes #919 --- class-two-factor-core.php | 6 +++--- tests/class-two-factor-core.php | 26 +++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 7c0ad9a0..df6e566c 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -1022,10 +1022,10 @@ public static function maybe_show_last_login_failure_notice( $user ) { echo '
'; printf( esc_html( - /* translators: 1: number of failed login attempts, 2: time since last failed attempt */ + /* translators: 1: number of failed verification code attempts, 2: human-readable time since the last attempt, e.g. "5 minutes" */ _n( - 'WARNING: Your account has attempted to login %1$s time without providing a valid two factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', - 'WARNING: Your account has attempted to login %1$s times without providing a valid two factor token. The last failed login occurred %2$s ago. If this wasn\'t you, you should reset your password.', + '%1$s failed verification code attempt on this account. The last attempt was %2$s ago. If you did not make this attempt, review your account security after logging in.', + '%1$s failed verification code attempts on this account. The last attempt was %2$s ago. If you did not make these attempts, review your account security after logging in.', $failed_login_count, 'two-factor' ) diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index 38052106..e1a44f98 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -797,8 +797,7 @@ public function test_maybe_show_last_login_failure_notice() { $this->assertNotEmpty( $contents ); $this->assertStringNotContainsString( '1 times', $contents ); - $this->assertStringContainsString( 'attempted to login', $contents ); - $this->assertStringContainsString( 'without providing a valid two factor token', $contents ); + $this->assertStringContainsString( 'failed verification code attempt', $contents ); // 5 failed login attempts 5 hours ago - User should be informed. $five_hours_ago = time() - 5 * HOUR_IN_SECONDS; @@ -809,10 +808,31 @@ public function test_maybe_show_last_login_failure_notice() { $contents = ob_get_clean(); $this->assertNotEmpty( $contents ); - $this->assertStringContainsString( '5 times', $contents ); + $this->assertStringContainsString( 'failed verification code attempts', $contents ); $this->assertStringContainsString( human_time_diff( $five_hours_ago ), $contents ); } + /** + * Test that the login failure notice uses calm, informational language. + * + * @covers Two_Factor_Core::maybe_show_last_login_failure_notice() + */ + public function test_login_failure_notice_language_is_calm_and_informational() { + $user = $this->get_dummy_user(); + update_user_meta( $user->ID, Two_Factor_Core::USER_FAILED_LOGIN_ATTEMPTS_KEY, 3 ); + update_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, time() - 60 ); + + ob_start(); + Two_Factor_Core::maybe_show_last_login_failure_notice( $user ); + $contents = ob_get_clean(); + + $this->assertStringNotContainsString( 'WARNING', $contents ); + $this->assertStringNotContainsString( "wasn't you", $contents ); + $this->assertStringNotContainsString( 'reset your password', $contents ); + $this->assertStringContainsString( 'failed verification code', $contents ); + $this->assertStringContainsString( 'review your account security', $contents ); + } + /** * Test no reset notice when no errors. * From 907c16e832b52776e6b7a81f859f54a662b45534 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Thu, 16 Jul 2026 11:29:37 -0600 Subject: [PATCH 2/3] test: pin singular/plural forms in login failure notice assertions The '1 times' assertion was a no-op against the reworded notice, and 'failed verification code attempt' also matches the plural 'attempts', so neither form was actually pinned. Assert the rendered singular and plural strings and that the plural is absent for a single failure. Also use MINUTE_IN_SECONDS instead of a bare 60, matching nearby tests. --- tests/class-two-factor-core.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index e1a44f98..75dc0f9e 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -796,8 +796,10 @@ public function test_maybe_show_last_login_failure_notice() { $contents = ob_get_clean(); $this->assertNotEmpty( $contents ); - $this->assertStringNotContainsString( '1 times', $contents ); - $this->assertStringContainsString( 'failed verification code attempt', $contents ); + // A single failure uses the singular form; assert the plural is absent, since + // "attempt" alone also matches "attempts". + $this->assertStringContainsString( '1 failed verification code attempt on this account', $contents ); + $this->assertStringNotContainsString( 'failed verification code attempts', $contents ); // 5 failed login attempts 5 hours ago - User should be informed. $five_hours_ago = time() - 5 * HOUR_IN_SECONDS; @@ -808,7 +810,7 @@ public function test_maybe_show_last_login_failure_notice() { $contents = ob_get_clean(); $this->assertNotEmpty( $contents ); - $this->assertStringContainsString( 'failed verification code attempts', $contents ); + $this->assertStringContainsString( '5 failed verification code attempts on this account', $contents ); $this->assertStringContainsString( human_time_diff( $five_hours_ago ), $contents ); } @@ -820,7 +822,7 @@ public function test_maybe_show_last_login_failure_notice() { public function test_login_failure_notice_language_is_calm_and_informational() { $user = $this->get_dummy_user(); update_user_meta( $user->ID, Two_Factor_Core::USER_FAILED_LOGIN_ATTEMPTS_KEY, 3 ); - update_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, time() - 60 ); + update_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, time() - MINUTE_IN_SECONDS ); ob_start(); Two_Factor_Core::maybe_show_last_login_failure_notice( $user ); From ad1ea7f87dd1c78ed3039fc1e04ddf2963f4d6e6 Mon Sep 17 00:00:00 2001 From: Dan Knauss Date: Fri, 17 Jul 2026 09:03:32 -0600 Subject: [PATCH 3/3] fix: give concrete, correct password guidance in login failure notice A failed 2FA attempt is only recorded after a correct password submission (wp_login -> show_two_factor_login issues the nonce; process_provider increments the counter only past verify_login_nonce). So if the account holder did not make the attempts, someone else knows their password, and a password change is exactly the right remedy. The prior 'review your account security after logging in' was vague, and the rationale for dropping the password advice was backwards. Keep the calm, no-WARNING framing but restore concrete, correct guidance. Also assert the rendered %2$s time in the calm-language test (it was seeded but never checked) and use MINUTE_IN_SECONDS instead of a bare 60. --- class-two-factor-core.php | 4 ++-- tests/class-two-factor-core.php | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index df6e566c..76a6f5a7 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -1024,8 +1024,8 @@ public static function maybe_show_last_login_failure_notice( $user ) { esc_html( /* translators: 1: number of failed verification code attempts, 2: human-readable time since the last attempt, e.g. "5 minutes" */ _n( - '%1$s failed verification code attempt on this account. The last attempt was %2$s ago. If you did not make this attempt, review your account security after logging in.', - '%1$s failed verification code attempts on this account. The last attempt was %2$s ago. If you did not make these attempts, review your account security after logging in.', + '%1$s failed verification code attempt on this account. The last attempt was %2$s ago. If you did not make this attempt, someone else may know your password. Change your password after you log in.', + '%1$s failed verification code attempts on this account. The last attempt was %2$s ago. If you did not make these attempts, someone else may know your password. Change your password after you log in.', $failed_login_count, 'two-factor' ) diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index 88c17c91..206f0558 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -832,9 +832,10 @@ public function test_maybe_show_last_login_failure_notice() { * @covers Two_Factor_Core::maybe_show_last_login_failure_notice() */ public function test_login_failure_notice_language_is_calm_and_informational() { - $user = $this->get_dummy_user(); + $user = $this->get_dummy_user(); + $one_minute_ago = time() - MINUTE_IN_SECONDS; update_user_meta( $user->ID, Two_Factor_Core::USER_FAILED_LOGIN_ATTEMPTS_KEY, 3 ); - update_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, time() - MINUTE_IN_SECONDS ); + update_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, $one_minute_ago ); ob_start(); Two_Factor_Core::maybe_show_last_login_failure_notice( $user ); @@ -842,9 +843,13 @@ public function test_login_failure_notice_language_is_calm_and_informational() { $this->assertStringNotContainsString( 'WARNING', $contents ); $this->assertStringNotContainsString( "wasn't you", $contents ); - $this->assertStringNotContainsString( 'reset your password', $contents ); $this->assertStringContainsString( 'failed verification code', $contents ); - $this->assertStringContainsString( 'review your account security', $contents ); + $this->assertStringContainsString( 'someone else may know your password', $contents ); + $this->assertStringContainsString( 'Change your password after you log in', $contents ); + $this->assertStringContainsString( + 'The last attempt was ' . human_time_diff( $one_minute_ago ) . ' ago', + $contents + ); } /**