Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Enhanced password and login security for WordPress.
- **Minimum length** enforcement (default 16) on top of the zxcvbn score.
- **Generic login errors** to prevent username enumeration on the wp-login form. Always on — no toggle. Only applies to authentication failures; password-reset and profile-update validation errors remain verbose so users can correct their input.
- **Safer multisite onboarding.** Auto-activates new signups server-side so no activation link is emailed and the `wp-activate.php` landing page (which would print username + plaintext password) is never reached. The welcome email is rewritten to contain a one-time password-reset link instead of a generated password.
- **Rate limiting**
- **Login limiting** via transient-based counters across three dimensions (IP+Username, IP, Username). Locks out brute-force attempts. Admin can clear all counters via Settings > Yard Warden.

## Hooks

Expand Down
20 changes: 10 additions & 10 deletions languages/yard_warden.pot
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@ msgstr ""
msgid "If an account with this username or email address exists, you will receive an email with a link to reset your password."
msgstr ""

# Rate limiting
#: src/RateLimit/RateLimitServiceProvider.php
# Login limiting
#: src/LimitLogin/LimitLoginServiceProvider.php
msgid "Too many failed login attempts. Please try again later."
msgstr ""

#: src/RateLimit/RateLimitAdminPage.php
msgid "Rate Limiting"
#: src/LimitLogin/LimitLoginAdminPage.php
msgid "Login Limiting"
msgstr ""

#: src/RateLimit/RateLimitAdminPage.php
msgid "If a legitimate user has been locked out due to too many failed login attempts, you can clear all rate limits below."
#: src/LimitLogin/LimitLoginAdminPage.php
msgid "If a legitimate user has been locked out due to too many failed login attempts, you can clear all login limits below."
msgstr ""

#: src/RateLimit/RateLimitAdminPage.php
msgid "Clear All Rate Limits"
#: src/LimitLogin/LimitLoginAdminPage.php
msgid "Clear All Login Limits"
msgstr ""

#: src/RateLimit/RateLimitAdminPage.php
msgid "All rate limits have been cleared."
#: src/LimitLogin/LimitLoginAdminPage.php
msgid "All login limits have been cleared."
msgstr ""

#. translators: %d: minimum password length
Expand Down
7 changes: 7 additions & 0 deletions src/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Yard\Warden\Login\LoginServiceProvider;
use Yard\Warden\Login\LostPasswordServiceProvider;
use Yard\Warden\Onboarding\OnboardingServiceProvider;
use Yard\Warden\LimitLogin\Limiter;
use Yard\Warden\LimitLogin\LimitLoginAdminPage;
use Yard\Warden\LimitLogin\LimitLoginServiceProvider;
use Yard\Warden\Password\PasswordServiceProvider;
use Yard\Warden\Password\PasswordUIServiceProvider;
use Yard\Warden\Password\StrengthValidator;
Expand Down Expand Up @@ -50,8 +53,12 @@ private static function registerPasswordProviders(): void

private static function registerLoginProviders(): void
{
$limiter = new Limiter();

(new LoginServiceProvider())->register();
(new LostPasswordServiceProvider())->register();
(new LimitLoginServiceProvider($limiter))->register();
(new LimitLoginAdminPage($limiter))->register();
}

private static function registerOnboardingProviders(): void
Expand Down
106 changes: 106 additions & 0 deletions src/LimitLogin/LimitLoginAdminPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace Yard\Warden\LimitLogin;

use Yard\Logging\Log;

/**
* Registers a native WordPress admin page under Settings > Yard Warden
* with a button to clear all login-limit transients.
*/
class LimitLoginAdminPage
{
private const NONCE_ACTION = 'yard_warden_clear_limit_login';
private const MENU_SLUG = 'yard-warden';

/** @var Limiter */
private $limiter;

public function __construct(Limiter $limiter)
{
$this->limiter = $limiter;
}

public function register(): void
{
add_action('admin_menu', [$this, 'registerPage']);
add_action('admin_init', [$this, 'handleClearRequest']);
}

public function registerPage(): void
{
add_options_page(
__('Yard Warden', YARD_WARDEN_TEXT_DOMAIN),
__('Yard Warden', YARD_WARDEN_TEXT_DOMAIN),
'manage_options',
self::MENU_SLUG,
[$this, 'renderPage']
);
}

public function handleClearRequest(): void
{
if (! isset($_POST['yard_warden_clear_limit_login'])) {
return;
}

if (! current_user_can('manage_options')) {
return;
}

check_admin_referer(self::NONCE_ACTION);

$deleted = $this->limiter->clearAll();

Log::warning(sprintf(
'Yard Warden: admin cleared all login limits (%d transient rows removed)',
$deleted
));

add_settings_error(
self::MENU_SLUG,
'limits_cleared',
__('All login limits have been cleared.', YARD_WARDEN_TEXT_DOMAIN),
'success'
);

set_transient('settings_errors', get_settings_errors(), 30);

wp_safe_redirect(add_query_arg('settings-updated', 'true', menu_page_url(self::MENU_SLUG, false)));
exit;
}

public function renderPage(): void
{
if (! current_user_can('manage_options')) {
return;
}

if (isset($_GET['settings-updated'])) {
settings_errors(self::MENU_SLUG);
}

?>
<div class="wrap">
<h1><?php esc_html_e('Yard Warden', YARD_WARDEN_TEXT_DOMAIN); ?></h1>

<div class="card" style="max-width: 600px;">
<h2><?php esc_html_e('Login Limiting', YARD_WARDEN_TEXT_DOMAIN); ?></h2>
<p><?php esc_html_e('If a legitimate user has been locked out due to too many failed login attempts, you can clear all login limits below.', YARD_WARDEN_TEXT_DOMAIN); ?></p>

<form method="post">
<?php wp_nonce_field(self::NONCE_ACTION); ?>
<?php submit_button(
__('Clear All Login Limits', YARD_WARDEN_TEXT_DOMAIN),
'secondary',
'yard_warden_clear_limit_login',
false
); ?>
</form>
</div>
</div>
<?php
}
}
Loading
Loading