Dynamically Using spatie/laravel-honeypot with Laravel Jetstream cover image

Dynamically Using spatie/laravel-honeypot with Laravel Jetstream

Chris Di Carlo • May 20, 2022

The Want

I've been having an issue lately with my SasS Triprecor with registration bots and wanted to leverage the spatie/laravel-honeypot package to protect it. The only problem is Jetstream uses Laravel Fortify under the hood and the auth-related routes are hidden inside the vendor folder. I could have just copied the Fortify routes file into my app, add the Honeypot middleware to the route(s), and then told Fortify not to register it's own routes automatically, but that seemed like a lot of steps for something I thought should be pretty simple. I wondered if it was possible to dynamically add the Honeypot middleware only on the routes I wanted. This let me down a bit of a rabbit hole delving into middleware.

The Journey

I have very rarely needed to create my own custom middleware but in this case I thought they might be the ticket to solving my problem. Unfortunately, it took a while for me to wrap my head around how it could work.

At first, I thought I could simply add the Honeypot middleware in the service provider based on the route URI but I couldn't get that to work.

Then I thought I could create a custom middleware and add it to the middleware group that Fortify uses. I would simply check if it's a POST request and do I want the Honeypot, e.g. the register route - then add the middleware accordingly. But alas, I couldn't find a way to add the middleware and actually have it take effect during that request lifecycle.

After staring at the code for a bit, I kept seeing the ->withoutMiddleware() method on the route and it dawned on me - rather than trying to make the conditional middleware additive, make it substractive.

Eureka!

Seeing the problem inverted brought the solution into sharp relief:

I immediately saw how this could work and within a couple of minutes had a working example.

The Code

In the end, I needed far less code than I would have thought. Without further ado, here's the code!

Add RemoveHoneypotMiddleware and ProtectAgainstSpam to the web middleware group. The order matters! The Honeypot middleware has to be after RemoveHoneypotMiddleware!

app/Http/Kernel.php

 protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Laravel\Jetstream\Http\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\RemoveHoneypotMiddleware::class,
            \Spatie\Honeypot\ProtectAgainstSpam::class,
        ],

        ...

app/Http/Middleware/RemoveHoneypotMiddleware.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Spatie\Honeypot\ProtectAgainstSpam;

class RemoveHoneypotMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        if (!$request->isMethod('POST')) {
            Route::getCurrentRoute()->withoutMiddleware(ProtectAgainstSpam::class);
            return $next($request);
        }

        if (!$request->is('register')) {
            Route::getCurrentRoute()->withoutMiddleware(ProtectAgainstSpam::class);
            return $next($request);
        }

        return $next($request);
    }
}

That's it! A proof of concept to integrate Honeypot into Jetstream without needing to duplicate the whole routes file. Let me know what you think!

Happy coding! Cheers