Securing your Laravel application from brute force login attacks should be one of your top priorities — especially if your app handles user accounts, sensitive data, or administrative access.
In this tutorial, we'll walk step-by-step through implementing real-world protection techniques using:
-
Google reCAPTCHA (to block bots)
-
Laravel throttle middleware (to block abusive IPs)
-
Custom database-level login lockout (based on username and IP address)
What Is a Brute Force Attack?
Before we jump into the solutions, let’s quickly understand what we’re protecting against.
A brute force attack is when an attacker sends repeated login requests with different credentials, trying to guess the correct username and password. If there’s no protection, they could eventually succeed and take over user accounts — or worse, your admin panel.
Luckily, Laravel provides some excellent tools, and we can add a few more layers to lock things down tightly.
Let’s build a layered defense system for your Laravel login process.
Step 1: Add CAPTCHA to Prevent Bot Logins
First, we'll use Google reCAPTCHA to ensure login attempts are made by real users — not automated scripts.
Step 1.1: Register for Google reCAPTCHA
Go to Google reCAPTCHA and:
-
Register your site
-
Choose reCAPTCHA v2 ("I'm not a robot")
-
Add your domain
-
Get your Site Key and Secret Key
Step 1.2: Install reCAPTCHA in Laravel
Use the community-supported package:
composer require anhskohbo/no-captcha
Then in config/app.php
, under providers (if not auto-discovered):
Anhskohbo\NoCaptcha\NoCaptchaServiceProvider::class,
Step 1.3: Add Keys in .env
NOCAPTCHA_SITEKEY=your-site-key
NOCAPTCHA_SECRET=your-secret-key
Step 1.4: Add CAPTCHA to Your Login Form
In your login Blade view:
{!! NoCaptcha::renderJs() !!}
{!! NoCaptcha::display() !!}
Step 1.5: Validate CAPTCHA in LoginController
$request->validate([
'username' => 'required',
'password' => 'required',
'g-recaptcha-response' => 'required|captcha',
]);
Step 2: Block IP Address Temporarily After Multiple Attempts (Middleware)
Let’s limit how many requests one IP can make in a minute. If they exceed the limit, they get blocked for a short time.
Step 2.1: Create Middleware to Track IP
php artisan make:middleware ThrottleLoginAttempts
Step 2.2: Middleware Logic
In ThrottleLoginAttempts.php
use Illuminate\Support\Facades\Cache;
public function handle($request, Closure $next)
{
$ip = $request->ip();
$key = "login_attempts_ip_{$ip}";
$attempts = Cache::get($key, 0);
if ($attempts >= 10) {
return response("Too many attempts from your IP. Try again later.", 429);
}
Cache::put($key, $attempts + 1, now()->addMinutes(1)); // reset in 1 minute
return $next($request);
}
This allows 10 attempts per IP per minute, then blocks for a minute.
Step 2.3: Register the Middleware
In app/Http/Kernel.php
protected $routeMiddleware = [
'throttle.ip' => \App\Http\Middleware\ThrottleLoginAttempts::class,
];
Apply it to your login route:
Route::post('/login', [LoginController::class, 'login'])->middleware('throttle.ip');
Step 3: Database-Level Lockout Based on Username and IP
Now let’s store and monitor login attempts in a database.
If a user enters the wrong password 5 times, we’ll block their login for 30 minutes — even if they keep switching IPs.
Step 3.1: Create Login Attempt Table
php artisan make:migration create_login_attempts_table
Migration file example:
Schema::create('login_attempts', function (Blueprint $table) {
$table->id();
$table->string('ip_address');
$table->string('username');
$table->integer('attempts')->default(0);
$table->timestamp('locked_until')->nullable();
$table->timestamps();
});
Then run:
php artisan migrate
Step 3.2: Add Logic in LoginController
In your login method:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\LoginAttempt;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
public function login(Request $request){
$request->validate([
'username' => 'required',
'password' => 'required',
'g-recaptcha-response' => 'required|captcha',
]);
$ip = $request->ip();
$username = $request->username;
$attempt = LoginAttempt::firstOrNew([
'ip_address' => $ip,
'username' => $username,
]);
$lockedUntil = $attempt->locked_until ? Carbon::parse($attempt->locked_until) : null;
if ($lockedUntil && $lockedUntil->isFuture()) {
$secondsLeft = $lockedUntil->diffInSeconds(now());
return back()->withInput()->withErrors([
'message' => "Your account is locked. Please wait " . gmdate('i\m s\s', $secondsLeft) . "."
]);
}
$credentials = Auth::attempt([
'username' => $request->username,
'password' => $request->password,
]);
if (!$credentials) {
$attempt->attempts += 1;
if ($attempt->attempts >= 5) {
$attempt->locked_until = now()->addMinutes(30);
$attempt->attempts = 0;
$attempt->save();
return back()->withInput()->withErrors([
'message' => "Too many failed attempts. Account is locked for 30 minutes."
]);
}
$attempt->save();
return back()->withInput()->withErrors([
'message' => "Invalid credentials. You have " . (5 - $attempt->attempts) . " attempts left."
]);
}
// SUCCESSFUL LOGIN
$attempt->delete(); // clear login record
return redirect()->intended('/dashboard');
}
}
This method tracks login attempts per IP + username and blocks brute force activity.
Security Tips
-
Use HTTPS on all routes.
-
Encourage users to use strong passwords.
-
Enable email alerts for suspicious login behavior.
-
Consider two-factor authentication for admin accounts.
-
Regularly review login activity logs.
Conclusion
Brute force attacks are very real but stopping them doesn’t have to be hard.
With a smart combo of CAPTCHA, middleware throttling, and database tracking, you can keep your Laravel app locked down like a fortress.
Start simple, then layer your defenses for maximum protection.