How to Add Bot Detection
to Laravel with Middleware
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=3600Step 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>
@endlowriskStep 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
| Setup | Avg Latency Added | Cache Hit Rate | API Calls Saved |
|---|---|---|---|
| No cache (every request) | ~45ms | 0% | 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.