IPASIS - IP Reputation and Risk Intelligence API
Blog/Integration Guide

How to Add Bot Detection to Laravel with Middleware

March 31, 202618 min read
🔺
Laravel + IPASIS

Laravel's middleware system sits at the heart of every HTTP request. Before a request reaches your controllers, routes, or Eloquent models, middleware can inspect, modify, or reject it. That makes middleware the perfect layer for bot detection — you can block malicious traffic before it ever touches your business logic.

In this guide, you'll integrate IPASIS IP intelligence into a Laravel application step by step. We'll start with a basic middleware and progressively add caching, route groups, form request validation, queue-based scoring, and structured logging.

📋

What You'll Build

  • ✅ Laravel middleware for IP risk scoring on every request
  • ✅ Cache integration (Redis / Memcached / file) for minimal API calls
  • ✅ Route group middleware for tiered protection levels
  • ✅ Form Request validation rule for bot-protected forms
  • ✅ Queue-based async scoring with Laravel Jobs
  • ✅ Rate limiting with risk-aware throttle groups
  • ✅ Blade directive for conditional UI rendering
  • ✅ Structured logging and monitoring via Laravel events
  • ✅ Complete production-ready setup with Sanctum/Fortify integration

Prerequisites

  • • Laravel 10+ (works with Laravel 11 too)
  • • PHP 8.1+
  • • An IPASIS API key (free tier: 1,000 lookups/day)
  • • Composer and a working Laravel installation
  • • Redis recommended (but not required — file/database cache works)

Step 1: Create the IPASIS Service Class

Before writing middleware, let's create a clean service class that handles all communication with the IPASIS API. This keeps your middleware thin and makes the service reusable across your entire application.

// app/Services/IpasisService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class IpasisService
{
    private string $apiKey;
    private string $baseUrl;
    private int $cacheTtl;
    private int $timeout;

    public function __construct()
    {
        $this->apiKey = config('services.ipasis.key');
        $this->baseUrl = config('services.ipasis.url', 'https://api.ipasis.com');
        $this->cacheTtl = config('services.ipasis.cache_ttl', 3600);
        $this->timeout = config('services.ipasis.timeout', 3);
    }

    /**
     * Look up IP risk data. Returns cached result if available.
     */
    public function lookup(string $ip): ?array
    {
        $cacheKey = "ipasis:risk:{$ip}";

        return Cache::remember($cacheKey, $this->cacheTtl, function () use ($ip) {
            try {
                $response = Http::timeout($this->timeout)
                    ->withHeaders([
                        'Authorization' => "Bearer {$this->apiKey}",
                        'Accept' => 'application/json',
                    ])
                    ->get("{$this->baseUrl}/v1/ip/{$ip}");

                if ($response->successful()) {
                    return $response->json();
                }

                Log::warning('IPASIS API error', [
                    'ip' => $ip,
                    'status' => $response->status(),
                ]);
                return null;
            } catch (\Exception $e) {
                Log::error('IPASIS API exception', [
                    'ip' => $ip,
                    'error' => $e->getMessage(),
                ]);
                return null; // Fail open — don't block on API errors
            }
        });
    }

    /**
     * Get just the risk score for an IP (0-100).
     */
    public function riskScore(string $ip): int
    {
        $data = $this->lookup($ip);
        return $data['risk_score'] ?? 0;
    }

    /**
     * Check if an IP is high risk (above threshold).
     */
    public function isHighRisk(string $ip, int $threshold = 70): bool
    {
        return $this->riskScore($ip) >= $threshold;
    }
}

Add the configuration to your config/services.php:

// config/services.php
'ipasis' => [
    'key' => env('IPASIS_API_KEY'),
    'url' => env('IPASIS_API_URL', 'https://api.ipasis.com'),
    'cache_ttl' => env('IPASIS_CACHE_TTL', 3600),
    'timeout' => env('IPASIS_TIMEOUT', 3),
],

And your .env:

IPASIS_API_KEY=your_api_key_here
IPASIS_CACHE_TTL=3600

Step 2: Basic Bot Detection Middleware

Now create the middleware. Laravel's php artisan make:middleware generates the scaffold, but here's a complete implementation:

// app/Http/Middleware/BotDetection.php
<?php

namespace App\Http\Middleware;

use App\Services\IpasisService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BotDetection
{
    public function __construct(
        private IpasisService $ipasis
    ) {}

    public function handle(Request $request, Closure $next, string $level = 'standard'): Response
    {
        $ip = $request->ip();
        $data = $this->ipasis->lookup($ip);

        // Attach risk data to the request for downstream use
        $request->attributes->set('ip_risk', $data);
        $request->attributes->set('ip_risk_score', $data['risk_score'] ?? 0);

        $threshold = match ($level) {
            'critical' => 50,  // Payments, admin — strict
            'high'     => 65,  // Signups, login — moderate
            'standard' => 80,  // General pages — permissive
            'monitor'  => 101, // Never block, just log
            default    => 80,
        };

        $score = $data['risk_score'] ?? 0;

        if ($score >= $threshold) {
            // Log the block
            activity()
                ->withProperties([
                    'ip' => $ip,
                    'risk_score' => $score,
                    'level' => $level,
                    'path' => $request->path(),
                    'is_vpn' => $data['is_vpn'] ?? false,
                    'is_proxy' => $data['is_proxy'] ?? false,
                    'is_tor' => $data['is_tor'] ?? false,
                    'is_datacenter' => $data['is_datacenter'] ?? false,
                ])
                ->log('bot_blocked');

            if ($request->expectsJson()) {
                return response()->json([
                    'error' => 'Request blocked due to suspicious activity.',
                    'code' => 'IP_RISK_HIGH',
                ], 403);
            }

            abort(403, 'Access denied. Your request has been flagged as suspicious.');
        }

        return $next($request);
    }
}

Step 3: Register Middleware and Route Groups

Register the middleware in your application. In Laravel 11 (using bootstrap/app.php):

// bootstrap/app.php (Laravel 11)
use App\Http\Middleware\BotDetection;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // Global middleware — runs on every request
        $middleware->append(BotDetection::class);

        // Or as named aliases for route-level use:
        $middleware->alias([
            'bot.critical' => BotDetection::class.':critical',
            'bot.high'     => BotDetection::class.':high',
            'bot.standard' => BotDetection::class.':standard',
            'bot.monitor'  => BotDetection::class.':monitor',
        ]);
    })

For Laravel 10, register in app/Http/Kernel.php:

// app/Http/Kernel.php (Laravel 10)
protected $middlewareAliases = [
    // ... existing aliases
    'bot.critical' => \App\Http\Middleware\BotDetection::class,
    'bot.high'     => \App\Http\Middleware\BotDetection::class,
    'bot.standard' => \App\Http\Middleware\BotDetection::class,
    'bot.monitor'  => \App\Http\Middleware\BotDetection::class,
];

Now apply different protection levels to your routes:

// routes/web.php

// Critical routes — strictest protection
Route::middleware(['bot.critical'])->group(function () {
    Route::post('/checkout', [CheckoutController::class, 'process']);
    Route::post('/payment/webhook', [PaymentController::class, 'webhook']);
});

// High protection — signups, login, password reset
Route::middleware(['bot.high'])->group(function () {
    Route::post('/register', [RegisterController::class, 'store']);
    Route::post('/login', [LoginController::class, 'authenticate']);
    Route::post('/forgot-password', [PasswordResetController::class, 'send']);
});

// Standard — general authenticated routes
Route::middleware(['auth', 'bot.standard'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::resource('/projects', ProjectController::class);
});

// API routes with bot detection
Route::middleware(['bot.high'])->prefix('api/v1')->group(function () {
    Route::post('/leads', [LeadController::class, 'store']);
    Route::post('/contact', [ContactController::class, 'submit']);
});

Step 4: Form Request Validation Rule

Laravel's Form Requests are perfect for adding bot detection as a validation rule. Create a custom rule that checks IP risk as part of form validation:

// app/Rules/NotBot.php
<?php

namespace App\Rules;

use App\Services\IpasisService;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class NotBot implements ValidationRule
{
    public function __construct(
        private int $threshold = 70
    ) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $ipasis = app(IpasisService::class);
        $ip = request()->ip();
        $score = $ipasis->riskScore($ip);

        if ($score >= $this->threshold) {
            $fail('This request has been flagged as suspicious. Please try again or contact support.');
        }
    }
}

// Usage in a Form Request:
// app/Http/Requests/RegisterRequest.php
<?php

namespace App\Http\Requests;

use App\Rules\NotBot;
use Illuminate\Foundation\Http\FormRequest;

class RegisterRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email' => ['required', 'email', 'unique:users', new NotBot(65)],
            'name' => ['required', 'string', 'max:255'],
            'password' => ['required', 'confirmed', 'min:8'],
        ];
    }
}

Step 5: Risk-Aware Rate Limiting

Laravel's built-in rate limiter can be extended to apply stricter limits to risky IPs. Configure this in your AppServiceProvider:

// app/Providers/AppServiceProvider.php
use App\Services\IpasisService;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    // Risk-aware API rate limiter
    RateLimiter::for('api', function ($request) {
        $ip = $request->ip();
        $riskScore = $request->attributes->get('ip_risk_score', 0);

        // High risk: 10 req/min | Medium: 30 req/min | Low: 60 req/min
        $maxAttempts = match (true) {
            $riskScore >= 70 => 10,
            $riskScore >= 40 => 30,
            default          => 60,
        };

        return Limit::perMinute($maxAttempts)->by($ip);
    });

    // Strict limiter for auth endpoints
    RateLimiter::for('login', function ($request) {
        $ip = $request->ip();
        $riskScore = $request->attributes->get('ip_risk_score', 0);

        // Risky IPs get 3 attempts per 5 minutes
        // Normal IPs get 10 attempts per 5 minutes
        $maxAttempts = $riskScore >= 50 ? 3 : 10;

        return Limit::perMinutes(5, $maxAttempts)
            ->by($request->input('email') . '|' . $ip);
    });

    // Signup rate limiter — prevent mass account creation
    RateLimiter::for('signup', function ($request) {
        $riskScore = $request->attributes->get('ip_risk_score', 0);

        return Limit::perHour($riskScore >= 50 ? 2 : 10)
            ->by($request->ip());
    });
}

Apply rate limiters to your routes:

Route::middleware(['bot.high', 'throttle:login'])
    ->post('/login', [LoginController::class, 'authenticate']);

Route::middleware(['bot.high', 'throttle:signup'])
    ->post('/register', [RegisterController::class, 'store']);

Route::middleware(['bot.standard', 'throttle:api'])
    ->prefix('api/v1')
    ->group(function () {
        // Your API routes
    });

Step 6: Queue-Based Async Scoring with Laravel Jobs

For non-critical paths where you want risk data but can't afford to add latency, push the scoring to a background job:

// app/Jobs/ScoreIpRisk.php
<?php

namespace App\Jobs;

use App\Models\User;
use App\Services\IpasisService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class ScoreIpRisk implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private string $ip,
        private ?int $userId = null,
        private string $context = 'general'
    ) {}

    public function handle(IpasisService $ipasis): void
    {
        $data = $ipasis->lookup($this->ip);
        $score = $data['risk_score'] ?? 0;

        // Log for analytics
        Log::channel('security')->info('IP risk scored', [
            'ip' => $this->ip,
            'user_id' => $this->userId,
            'risk_score' => $score,
            'is_vpn' => $data['is_vpn'] ?? false,
            'is_proxy' => $data['is_proxy'] ?? false,
            'is_datacenter' => $data['is_datacenter'] ?? false,
            'context' => $this->context,
        ]);

        // Flag user if risk is high
        if ($this->userId && $score >= 70) {
            $user = User::find($this->userId);
            if ($user) {
                $user->update([
                    'risk_flagged' => true,
                    'risk_score' => $score,
                    'risk_flagged_at' => now(),
                ]);

                // Optional: notify admin
                // Notification::route('slack', config('services.slack.security'))
                //     ->notify(new HighRiskUserDetected($user, $data));
            }
        }
    }
}

// Dispatch from a controller:
// ScoreIpRisk::dispatch($request->ip(), auth()->id(), 'dashboard_visit');

Step 7: Blade Directive for Conditional UI

Show or hide UI elements based on IP risk. Register a custom Blade directive:

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    // @lowrisk ... @endlowrisk — show only to low-risk visitors
    Blade::if('lowrisk', function (int $threshold = 40) {
        $score = request()->attributes->get('ip_risk_score', 0);
        return $score < $threshold;
    });
}

// In your Blade templates:
// resources/views/register.blade.php

@lowrisk
    {{-- Normal registration form --}}
    <form method="POST" action="/register">
        @csrf
        <input type="text" name="name" placeholder="Name" />
        <input type="email" name="email" placeholder="Email" />
        <input type="password" name="password" placeholder="Password" />
        <button type="submit">Create Account</button>
    </form>
@else
    {{-- Show CAPTCHA or additional verification --}}
    <form method="POST" action="/register">
        @csrf
        <input type="text" name="name" placeholder="Name" />
        <input type="email" name="email" placeholder="Email" />
        <input type="password" name="password" placeholder="Password" />
        <div id="captcha-widget"></div>
        <p class="text-sm text-gray-500">
            Additional verification required for security.
        </p>
        <button type="submit">Create Account</button>
    </form>
@endlowrisk

Step 8: Sanctum & Fortify Integration

If you're using Laravel Sanctum for API auth or Fortify for authentication, integrate bot detection into the authentication pipeline:

// Fortify: Custom login pipeline
// app/Actions/Fortify/AuthenticateWithRiskCheck.php
<?php

namespace App\Actions\Fortify;

use App\Services\IpasisService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Laravel\Fortify\Fortify;

class AuthenticateWithRiskCheck
{
    public function __invoke(Request $request, $next)
    {
        $ipasis = app(IpasisService::class);
        $data = $ipasis->lookup($request->ip());
        $score = $data['risk_score'] ?? 0;

        // Block high-risk login attempts entirely
        if ($score >= 80) {
            Log::channel('security')->warning('High-risk login blocked', [
                'ip' => $request->ip(),
                'email' => $request->input('email'),
                'risk_score' => $score,
            ]);

            throw ValidationException::withMessages([
                Fortify::username() => [
                    'Login temporarily blocked. Please try again later or contact support.',
                ],
            ]);
        }

        // Force 2FA for medium-risk logins (if 2FA is configured)
        if ($score >= 50) {
            session(['force_2fa' => true]);
        }

        return $next($request);
    }
}

// Register in FortifyServiceProvider:
// Fortify::authenticateThrough(fn () => [
//     AuthenticateWithRiskCheck::class,
//     AttemptToAuthenticate::class,
//     PrepareAuthenticatedSession::class,
// ]);
// Sanctum: Protect API token creation
// routes/api.php
Route::middleware(['bot.critical'])->post('/tokens/create', function (Request $request) {
    $riskScore = $request->attributes->get('ip_risk_score', 0);

    // Log the token creation with risk context
    $token = $request->user()->createToken(
        $request->input('token_name'),
        $request->input('abilities', ['*'])
    );

    Log::channel('security')->info('API token created', [
        'user_id' => $request->user()->id,
        'ip' => $request->ip(),
        'risk_score' => $riskScore,
        'token_name' => $request->input('token_name'),
    ]);

    return ['token' => $token->plainTextToken];
});

Step 9: Event-Driven Logging & Monitoring

Use Laravel's event system for clean separation between detection and response:

// app/Events/SuspiciousRequestDetected.php
<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class SuspiciousRequestDetected
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public string $ip,
        public int $riskScore,
        public array $riskData,
        public string $path,
        public ?int $userId = null
    ) {}
}

// app/Listeners/LogSuspiciousRequest.php
<?php

namespace App\Listeners;

use App\Events\SuspiciousRequestDetected;
use Illuminate\Support\Facades\Log;

class LogSuspiciousRequest
{
    public function handle(SuspiciousRequestDetected $event): void
    {
        Log::channel('security')->warning('Suspicious request', [
            'ip' => $event->ip,
            'risk_score' => $event->riskScore,
            'path' => $event->path,
            'user_id' => $event->userId,
            'is_vpn' => $event->riskData['is_vpn'] ?? false,
            'is_proxy' => $event->riskData['is_proxy'] ?? false,
            'is_tor' => $event->riskData['is_tor'] ?? false,
            'is_datacenter' => $event->riskData['is_datacenter'] ?? false,
            'country' => $event->riskData['country_code'] ?? 'unknown',
            'asn' => $event->riskData['asn'] ?? 'unknown',
        ]);
    }
}

// Dispatch from middleware:
// SuspiciousRequestDetected::dispatch(
//     $ip, $score, $data, $request->path(), auth()->id()
// );

Step 10: Complete Production Setup

Here's the full production middleware that combines everything — caching, tiered protection, events, and graceful degradation:

// app/Http/Middleware/BotDetection.php — Production Version
<?php

namespace App\Http\Middleware;

use App\Events\SuspiciousRequestDetected;
use App\Services\IpasisService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class BotDetection
{
    // Trusted IPs that skip bot detection
    private array $allowlist = [
        '127.0.0.1',
        '::1',
        // Add your monitoring IPs, CI servers, etc.
    ];

    // Paths that should never be blocked
    private array $excludedPaths = [
        'health',
        'up',
        '_debugbar/*',
    ];

    public function __construct(
        private IpasisService $ipasis
    ) {}

    public function handle(Request $request, Closure $next, string $level = 'standard'): Response
    {
        $ip = $request->ip();

        // Skip for trusted IPs
        if (in_array($ip, $this->allowlist)) {
            return $next($request);
        }

        // Skip for excluded paths
        if ($request->is(...$this->excludedPaths)) {
            return $next($request);
        }

        $data = $this->ipasis->lookup($ip);
        $score = $data['risk_score'] ?? 0;

        // Always attach risk data for downstream use
        $request->attributes->set('ip_risk', $data);
        $request->attributes->set('ip_risk_score', $score);

        $threshold = match ($level) {
            'critical' => 50,
            'high'     => 65,
            'standard' => 80,
            'monitor'  => 101,
            default    => 80,
        };

        // Log suspicious activity (score >= 40, even if below threshold)
        if ($score >= 40) {
            SuspiciousRequestDetected::dispatch(
                $ip, $score, $data ?? [], $request->path(), auth()->id()
            );
        }

        // Block if above threshold
        if ($score >= $threshold) {
            Log::channel('security')->info('Request blocked', [
                'ip' => $ip,
                'score' => $score,
                'level' => $level,
                'path' => $request->path(),
                'method' => $request->method(),
                'user_agent' => $request->userAgent(),
            ]);

            if ($request->expectsJson()) {
                return response()->json([
                    'error' => 'Request blocked.',
                    'code' => 'IP_RISK_BLOCKED',
                ], 403);
            }

            return response()->view('errors.blocked', [
                'support_email' => config('app.support_email'),
            ], 403);
        }

        $response = $next($request);

        // Add risk score header in non-production (helpful for debugging)
        if (app()->isLocal()) {
            $response->headers->set('X-Risk-Score', (string) $score);
        }

        return $response;
    }
}

Performance Benchmarks

SetupAvg Latency AddedCache Hit RateAPI Calls Saved
No cache (every request)~45ms0%0%
File cache (1h TTL)~5ms~85%~85%
Redis cache (1h TTL)~1ms~92%~92%
Redis + Queue (non-critical)<1ms~95%~95%

Summary

Here's what you've built — a complete bot detection integration for Laravel:

  • Service class — Clean, testable IPASIS API wrapper with caching
  • Tiered middleware — Critical / High / Standard / Monitor protection levels
  • Route groups — Different protection for auth, payments, API, and general pages
  • Form Request rule — Bot detection as a validation rule
  • Risk-aware rate limiting — Stricter limits for suspicious IPs
  • Background scoring — Queue-based non-blocking risk assessment
  • Blade directive — Show/hide UI based on risk level
  • Sanctum/Fortify integration — Risk-aware authentication pipeline
  • Event-driven logging — Clean separation of detection and response

Related Guides

Start Detecting Bots in Your Laravel App

Get your free IPASIS API key — 1,000 lookups per day, no credit card required. Works with Laravel 10, 11, and beyond.