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.