Notifying Users of Multiple Failed Login Attempts

This article is a follow-up to Implementing Suspicious Login Detection & "Wasn't Me" in Laravel . This time, the goal is to notify users when someone might be trying to brute-force their account. As always with Laravel, it's easy to implement.

Before We Start

Make sure you're using Laravel's default rate limiter. If you've customized it in config/fortify.php, reset the login limiter to null:

'limiters' => [
    'login' => null,
    'two-factor' => 'two-factor',
],

This ensures Laravel uses its default throttling behavior, which triggers a Lockout event after 5 failed login attempts within 60 seconds.

Note: You can modify how many login attempts a user may have within a period of time inside FortifyServiceProvider.

Creating the Listener

Let's create a listener for the Lockout event, but first, a test:

php artisan make:test SendLockoutNotificationTest

First, I want to verify that our listener is actually attached to the Lockout event:

it('is attached to auth lockout event', function () {
    Event::fake();
    Event::assertListening(
        Lockout::class,
        SendLockoutNotification::class
    );
});

Now let's create the SendLockoutNotification listener:

php artisan make:listener SendLockoutNotification --event=\\Illuminate\\Auth\\Events\\Lockout

Greenlight.

Sending the Notification

The next step is simple: notify the user about multiple failed login attempts. Let's update our test:

it('sends notification mail to user', function () {
    Notification::fake();

    $user = User::factory()->create();

    $request = Request::create(fake()->url, 'GET', [
        'email' => $user->email,
        'password' => fake()->password,
    ]);

    event(new Lockout($request));

    Notification::assertSentTo(
        $user,
        MultipleFailedLoginAttempts::class
    );
});

We should also make sure we don't send notifications (or leak information) when someone tries to brute-force a non-existent email:

it('does not send notification for non-existent email', function () {
    Notification::fake();

    $request = Request::create(fake()->url, 'GET', [
        'email' => 'nobody@example.com',
        'password' => fake()->password,
    ]);

    event(new Lockout($request));

    Notification::assertNothingSent();
});

Now let's apply the corresponding changes to the SendLockoutNotification listener:

public function handle(Lockout $event): void
{
    $user = User::whereEmail($event->request->email)->first();

    if (! $user) {
        return;
    }

    Notification::send($user, new MultipleFailedLoginAttempts);
}

Greenlight.

The Notification

The MultipleFailedLoginAttempts notification is straightforward:

public function toMail(object $notifiable): MailMessage
{
    /** @var User $notifiable */
    $token = Password::createToken($notifiable);

    $payload = [
        'token' => $token,
        'email' => $notifiable->email,
    ];

    return (new MailMessage)
        ->greeting("Hi {$notifiable->name},")
        ->line('We noticed several unsuccessful attempts to access your account on ' . config('app.name') . '.')
        ->line('If this was you, please double-check your login credentials.')
        ->line(
            'If it wasn\'t, we recommend contacting our support team. You can reach us at [' .
            config('app.emails.security') .
            '](mailto:' .
            config('app.emails.security') . ').'
        )
        ->line('You should also consider changing your password.')
        ->action(
            'Reset password',
            route('password.reset', $payload)
        );
}

To preview the notification during development:

Route::get('/notification', function () {
    $user = User::find(1);

    return (new MultipleFailedLoginAttempts)
        ->toMail($user);
});

Greenlight.

Afterword

That's it — users will now be notified when multiple failed login attempts occur on their account. Combined with the suspicious login detection from the previous article, you've got solid coverage for SOC 2 alerting requirements.

Have something in mind?

A project, a question, or just a hello.

Your email
Subject
Message