Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Simple Bruteforce Bundle Laravel Package

evozon-php/simple-bruteforce-bundle

A Symfony bundle that adds simple brute-force protection to your application, tracking failed login attempts and blocking or throttling repeated authentication failures to help defend against credential-guessing and password-spraying attacks.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/bruteforce.php. You cannot composer require it into a Laravel app and get anything useful — the auto-discovery will simply not fire. A Laravel developer evaluating this should treat it as reference architecture for a failed-login counter, or as something you port, not install. If you are in a Symfony codebase, proceed below.

Minimal steps (Symfony):

  1. composer require evozon-php/simple-bruteforce-bundle — check the composer.json constraints before you commit. A "Symfony 3+" bundle from this era typically caps at ^3.4|^4.0, which means it will not install cleanly on Symfony 5/6/7 without a fork and a composer.json bump.
  2. Register it: Symfony 3 → app/AppKernel.php registerBundles(); Symfony 4+ Flex → add to config/bundles.php.
  3. Read the config tree before writing any YAML. The real schema lives in vendor/evozon-php/simple-bruteforce-bundle/DependencyInjection/Configuration.php. Do not trust the README; it is usually stale on a package like this. bin/console config:dump-reference evozon_php_simple_bruteforce (adjust the alias) gives you the authoritative keys — typically max_attempts, ban_time/ban_duration, and a storage/driver key.
  4. Find the services: bin/console debug:container bruteforce and open Resources/config/services.xml. Whatever the manager is called, that is your entry point.
  5. Find the hook points: bin/console debug:event-dispatcher security.authentication.failure — if the bundle ships a listener, it is almost certainly hanging off the security failure/success events.

First use case: the login form. The mental model is identical to Laravel's RateLimiter::for('login', ...) plus the throttle middleware, except you wire it manually:

  • Before AuthenticationUtils / authenticate(), ask the manager isBanned($key) and short-circuit with a 429 (or re-render the form with a generic error).
  • On security.authentication.failure, record an attempt for that key.
  • On security.authentication.success, reset the counter for that key.

If the bundle already subscribes to those two events, steps 2 and 3 are free and you only write step 1.

Implementation Patterns

Pattern 1 — Event-driven, zero controller code. The cleanest integration: let the bundle's subscriber record failures and clear on success, and only add a guard in your login controller (or a small kernel.request listener scoped to _route === 'app_login'). This is the pattern to prefer because it also covers form login, JSON login, and any custom authenticator — the security events are authenticator-agnostic.

Pattern 2 — Explicit manager injection. Inject the manager (BruteForceManager / BruteForceHandler / whatever debug:container shows) into your controller and call the three primitives yourself. More code, but testable and obvious:

if ($this->bruteForce->isBanned($key)) {
    return $this->render('security/login.html.twig', ['error' => 'Too many attempts.']);
}
// ... attempt auth ...

Reach for this when you have a non-standard login flow (API token endpoint, OTP verification, password reset) where the bundle's event listeners do not apply.

Pattern 3 — Annotation/attribute guarding. If your version ships a @BruteForceProtection-style annotation, it is applied at the controller-action level. It is convenient but coarse: it keys off the request, not the submitted username, so it degrades to IP throttling. Do not rely on it alone for a login form.

Pattern 4 — Composite keys. The single most important design decision. ip alone lets one attacker behind NAT lock out an office; username alone lets an attacker lock out any user they can name (a free DoS). Use $request->getClientIp() . '|' . $username for the ban decision, and optionally keep a looser IP-only counter as a second tier. Laravel's RateLimiter examples do exactly this and it is worth copying the shape.

Pattern 5 — Storage backend. If the bundle defaults to Doctrine, you get persistence across deploys and multiple app servers — good. If it defaults to session, it is a speed bump only: clear the cookie, reset the counter. If it offers a cache-pool driver, point it at Redis so a load-balanced pair shares state. Swapping storage is usually a service alias override in services.yaml, not a config flag.

Pattern 6 — Response semantics. Return 429 Too Many Requests with a Retry-After header computed from the ban expiry, for API endpoints. For HTML login, re-render the form with a message that does not distinguish "banned" from "wrong password" — otherwise you have built a username enumeration oracle. Keep timing roughly constant too.

Testing workflow. Functional test with KernelBrowser: POST bad credentials maxAttempts times, assert the next POST is rejected without hitting the user provider, then travel the clock (ClockMock::withClockMock() around the ban window) and assert you can try again. Assert the counter is cleared after a successful login — that is the regression that actually bites in production.

Laravel mapping, for context: RateLimiter::for('login', fn ($r) => Limit::perMinute(5)->by($r->input('email').$r->ip())) + ->middleware('throttle:login') gives you a sliding window with a Retry-After and no database. This bundle gives you a harder ban (block for N minutes after M failures) rather than a sliding rate limit. They are different shapes: throttling degrades, banning locks out. Decide which you want before adopting.

Gotchas and Tips

  • Zero stars, zero dependents, Symfony 3+. That is not a neutral signal. Budget for owning the code: pin the exact version, and be ready to fork and patch composer.json for modern Symfony. If the requirement is simply "lock out after N failed logins," ~80 lines of your own listener plus a unique-indexed counter table is often cheaper than the integration debt.
  • IP behind a proxy is wrong. getClientIp() returns the load balancer unless framework.trusted_proxies / trusted_headers are configured. Get this wrong and your counter is global across all users — the whole site locks out at once. Verify with a log line before you ship.
  • Ban window semantics. Check whether the counter resets on ban expiry or whether attempts keep accumulating. A sliding window that never resets means a slow attacker is permanently banned while a legitimate fat-fingered user is locked out for good. Read the manager's isBanned()/addAttempt() implementations rather than guessing from config key names.
  • Off-by-one on maxAttempts. Does "5 attempts" mean the 5th fails or the 6th is blocked? Write the functional test that asserts the exact boundary; nobody gets this right from the docs.
  • Concurrent failures race. Two parallel bad logins can both read count = 4 and both write 5. If storage is Doctrine, the counter row needs an atomic upsert or a unique constraint with a retry — not read-modify-write in PHP.
  • Cleanup. Ban rows for abandoned keys accumulate forever. There is rarely a purge command shipped. Add a cron/console command deleting rows older than the ban window, or the table grows unbounded.
  • Cache/config staleness. After changing YAML, bin/console cache:clear. A stale container is the #1 cause of "my config isn't taking effect" in Symfony.
  • Extension points, in order of preference: (1) override config keys; (2) alias the storage/manager service to your own implementation in services.yaml; (3) register your own listener on security.authentication.failure with a higher priority so it runs before the bundle's; (4) decorate the manager service (decorates:). Avoid editing vendor files — you will lose it on the next composer update.
  • Debugging checklist: debug:container <name>, debug:event-dispatcher security.authentication.failure, debug:config <alias>, and a temporary dump() in the listener to confirm it fires at all. If attempts are not being recorded, 90% of the time the listener isn't registered or the event name is wrong for your Symfony version (security.authentication.failure vs the older security.authentication.failure on AuthenticationEvents).
  • Do not log the attempted password as part of recording the failure. Obvious, but bundles from this era sometimes dump the request bag into the attempt record.
  • If you port it to Laravel: use the cache with RateLimiter, or a login_attempts table with a composite unique index on (key, ip), a LoginThrottle service, and a ThrottleRequests-style middleware. You will get a better-maintained result than adopting a dormant Symfony 3 bundle through a compatibility shim.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings