nowo-tech/login-throttle-bundle
Symfony bundle that throttles login attempts to defend against brute-force attacks. Configure failed-attempt limits, lockout windows, and custom responses, with seamless integration into Symfony Security for safer authentication.
First, a reality check. nowo-tech/login-throttle-bundle is a Symfony bundle (symfony/framework-bundle + symfony/security-bundle, registered via config/bundles.php), not a Laravel package. You cannot composer require it into a Laravel app and expect anything to boot: there is no ServiceProvider, no config/app.php entry, no publishable config, and Laravel's package discovery has nothing to discover. The value for a Laravel developer is either (a) you are actually working in a Symfony codebase and need login throttling, or (b) you want to copy its defaults into Laravel's own RateLimiter. Be explicit about which one you're doing before you install anything.
If you are on Symfony, minimal path:
composer require nowo-tech/login-throttle-bundle
config/bundles.php (Flex may have added it).README.md first, then the two files that define everything that matters: src/DependencyInjection/LoginThrottleExtension.php and src/DependencyInjection/Configuration.php. Those tell you the real config keys and whether the bundle writes security.firewalls.*.login_throttling for you or just ships a recipe/config snippet.config/packages/security.yaml — the bundle's whole job is producing something equivalent to Symfony's native:security:
firewalls:
main:
login_throttling:
max_attempts: 5
interval: '15 minutes'
username + IP at 5 per 15 minutes, return HTTP 429 with Retry-After, and keep the login form message generic.For the Laravel baseline you are probably comparing against, the equivalent is ~15 lines and requires no third-party package:
// AppServiceProvider::boot()
RateLimiter::for('login', fn (Request $r) => Limit::perMinutes(15, 5)
->by(Str::lower($r->input('email')).'|'.$r->ip()));
// LoginRequest
if (RateLimiter::tooManyAttempts($key = 'login:'.$this->throttleKey(), 5)) {
throw ValidationException::withMessages(['email' => __('auth.throttle',
['seconds' => RateLimiter::availableIn($key)])]);
}
Per-firewall scoping is the unit of configuration. Symfony applies login_throttling per firewall, so the real pattern is one block per entry point you expose — main (form login), admin (separate admin form), and not your stateless API firewall unless it has a login authenticator. In Laravel terms: one named RateLimiter::for() per guard/route group, not one global limiter.
Delegate storage to a shared backend once you have more than one app server. The counters live in a cache pool. On a single box the default is fine; behind a load balancer or in a rolling-deploy environment, per-host counters mean an attacker gets 5 × N attempts. Prefer defining an explicit limiter and pointing the throttle at it so the storage is Redis/cache.app shared:
framework:
rate_limiter:
login_limiter:
policy: 'fixed_window'
limit: 5
interval: '15 minutes'
security:
firewalls:
main:
login_throttling:
limiter: login_limiter
This mirrors Laravel's Cache::store('redis') choice for RateLimiter — same reasoning, same failure mode if you skip it.
Layer it, don't replace it. Login throttling is one layer. Keep password-reset and 2FA-verification throttles separate (Laravel: RateLimiter::for('passwords'); Symfony: a second limiter on those controllers). Sharing one bucket lets an attacker burn your login budget to lock out a victim.
Hook the security events for observability. LoginFailureEvent / LoginSuccessEvent (or Laravel's Illuminate\Auth\Events\Lockout + Failed) are where you log lockouts and clear counters on success. Native Symfony throttling already clears on successful auth; your job is the telemetry, because a silent 429 storm is indistinguishable from an outage.
Integration tip: if you have custom authenticators (API key, magic link, SSO callback), login_throttling does not cover them. Apply the same limiter manually in the authenticator's authenticate() or in a kernel listener — the analogous Laravel move is calling RateLimiter::hit() inside your custom guard rather than assuming throttle: middleware reached it.
Testing pattern: functional test that posts bad credentials max_attempts + 1 times and asserts the final response is 429 with a Retry-After header. One focused test next to the existing login test is enough; do not build a throttling test suite for a config wrapper.
composer.lock, and read the DI extension before trusting it in production. If it's genuinely 200 lines of YAML sugar, copying that YAML into your own security.yaml removes the dependency entirely and is the sanest option for a Laravel-minded dev.login_throttling only guards the form-login authenticator. JSON login on the same firewall is covered; custom authenticators, remember-me re-auth, impersonation, and password reset are not. Verify with a test rather than assuming.trusted_proxies behind a load balancer, every request looks like the balancer's IP and one attacker locks out everybody. Same trap as $request->ip() returning 127.0.0.1 in Laravel when TrustProxies is wrong.interval is a string duration ('15 minutes', '1 hour'), not seconds. A bare integer silently means something else — check Configuration.php for the accepted type rather than guessing.config/packages/test/) or your second run of the same test starts already locked out. This is the single most common "why is CI red" cause with throttling.username + IP (the default) rather than username alone, so unknown users and real users behave identically; also avoid a distinct message like "account locked" that only ever appears for valid accounts.framework.rate_limiter definition (storage, policy, interval), LoginFailureEvent/LoginSuccessEvent listeners for logging and manual reset-style clearing, and — last resort — decorating the bundle's service. Nothing here warrants editing vendor/.RateLimiter) will not give it. Persist lockout events yourself from a listener/event subscriber.anyx/login-gate-bundle. If you are migrating, diff the behaviour (key composition, lockout duration, reset-on-success) rather than swapping the dependency and assuming parity; write one test per behaviour you care about during the migration and delete the extras afterwards.How can I help you explore Laravel packages today?