imran / laravel-watchtower
Intelligent Adaptive Rate Limiter & Security Shield for Laravel - Advanced threat detection, behavioral analysis, fingerprinting, and auto-hardening protection.
Requires
- php: ^8.1|^8.2|^8.3|^8.4
- ext-hash: *
- ext-json: *
- guzzlehttp/guzzle: ^7.0
- illuminate/cache: ^10.0|^11.0|^12.0|^13.0
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/redis: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- laravel/framework: ^10.0|^11.0|^12.0|^13.0
- mockery/mockery: ^1.5|^2.0
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
- ext-redis: Required for Redis storage driver (recommended for production)
- predis/predis: Alternative Redis client if ext-redis is not available
README
Intelligent Adaptive Rate Limiter & Security Shield for Laravel
Laravel Watchtower is a production-ready security package that goes far beyond Laravel's default rate limiting. It acts as a lightweight application firewall, providing adaptive rate limiting, behavioral analysis, fingerprinting, threat scoring, bot/scraper detection, auto-hardening, and progressive penalties.
๐ Features
Core Features
| Feature | Description |
|---|---|
| โ Adaptive Rate Limiting | Dynamic limits that change based on threat score |
| โ User Fingerprinting | Multi-factor fingerprinting beyond IP tracking |
| โ Threat Scoring System | Real-time risk assessment for every request |
| โ Behavioral Analysis | Timing patterns, navigation flow, entropy analysis |
| โ Bot & Scraper Detection | Headless browser detection, header analysis |
| โ Auto-Hardening | Automatically strengthens defenses during attacks |
| โ Progressive Penalties | Escalating consequences: slowdown โ throttle โ block |
| โ Honey Endpoints | Trap endpoints that detect scanners and bots |
| โ Distributed Attack Detection | Detect coordinated attacks across multiple sources |
| โ Endpoint Sensitivity Profiles | Different protection levels for different routes |
Threats It Solves
- Credential stuffing & brute force attacks
- Web scraping & API abuse
- Bot traffic & headless automation
- Session abuse & rotating IP attacks
- Endpoint probing & account enumeration
- DDoS & burst attacks
๐ Requirements
- PHP 8.1, 8.2, 8.3, or 8.4
- Laravel 10.x, 11.x, 12.x, or 13.x
- Redis (strongly recommended for production)
- Composer
๐ง Installation
composer require imran/laravel-watchtower
Quick Setup
php artisan watchtower:install php artisan migrate
This will publish the config, migrations, views, and language files.
Manual Setup
Publish configuration:
php artisan vendor:publish --tag=watchtower-config php artisan vendor:publish --tag=watchtower-migrations php artisan migrate
โ๏ธ Configuration
Configure via config/watchtower.php or environment variables:
WATCHTOWER_STORAGE=redis WATCHTOWER_REDIS_CONNECTION=default WATCHTOWER_KEY_PREFIX=watchtower WATCHTOWER_ADAPTIVE_ENABLED=true WATCHTOWER_BOT_DETECTION=true WATCHTOWER_BEHAVIOR_ENABLED=true WATCHTOWER_FINGERPRINT_ENABLED=true WATCHTOWER_AUTO_HARDEN=true WATCHTOWER_HONEY_ENABLED=true WATCHTOWER_BLOCK_MESSAGE=Access denied by security system.
Key Configuration Sections
profiles- Define sensitivity profiles for different endpointspenalties- Progressive penalty levels and durationsadaptive- Dynamic limit adjustment settingsthreat_scoring- Score thresholds and decay settingsbehavior- Behavioral analysis configurationbot_detection- Bot detection rules and patternsauto_hardening- Auto-hardening behaviorhoney_endpoints- Fake endpoints for bot trappingfingerprint- Fingerprinting componentsexempt- Paths and IPs that bypass protection
๐ฏ Usage
Middleware
Watchtower protects your app automatically, out of the box. When the package boots, it
pushes itself (with the default profile) onto both the web and api middleware groups โ
every route already gets baseline adaptive rate limiting with no setup required. You only need
to reach for explicit middleware below when you want a different profile than default for
specific routes (e.g. tighter limits on /login).
Protect a route group with a specific profile:
Route::middleware('watchtower:login')->group(function () { Route::post('/login', [AuthController::class, 'login']); Route::post('/register', [AuthController::class, 'register']); });
With profile-specific protection:
Route::middleware('watchtower:login')->post('/login', [AuthController::class, 'login']); Route::middleware('watchtower:api')->prefix('api')->group(function () { Route::get('/users', [UserController::class, 'index']); }); Route::middleware('watchtower:critical')->post('/password/reset', [AuthController::class, 'resetPassword']); Route::middleware('watchtower:scrape-sensitive')->get('/search', [SearchController::class, 'search']);
Fluent API
use Imran\Watchtower\Facades\Watchtower; // Basic protection Watchtower::protect('login') ->adaptive() ->detectBots() ->trackFingerprint() ->autoHarden() ->captchaAfter(3) ->banAfter(10);
Facade Methods
// Fluent protection Watchtower::protect('api')->adaptive()->detectBots(); // Process a request manually $decision = Watchtower::process($request, 'login'); // Check if an identifier is blocked $isBlocked = watchtower_is_blocked($identifier); // Get protection headers $headers = Watchtower::getHeaders(); // Get the last decision $decision = Watchtower::getDecision();
CAPTCHA / Challenge configuration
When captchaAfter(N) (or a profile's force_captcha_after) is triggered, Watchtower serves
a challenge page at /_watchtower/challenge. Configure a real CAPTCHA provider so it's actually
enforced server-side:
WATCHTOWER_CAPTCHA_PROVIDER=turnstile # turnstile | recaptcha | hcaptcha WATCHTOWER_CAPTCHA_SITE_KEY=your-site-key WATCHTOWER_CAPTCHA_SECRET_KEY=your-secret-key
If no site/secret key is configured, Watchtower automatically falls back to a server-verified
cooldown challenge (WATCHTOWER_CHALLENGE_COOLDOWN, default 15s) instead of pretending to
verify a CAPTCHA that isn't set up. The wait is timed from a server-side timestamp, not a
client-supplied value, so it can't be bypassed by submitting the form immediately.
A verified pass is remembered per-identifier for WATCHTOWER_CHALLENGE_PASS_TTL minutes
(default 30), so a legitimate user who already proved they're human isn't forced through the
challenge again on every request. Watchtower's own /_watchtower/* routes are always exempt
from its own middleware, so a blocked or challenged identifier can never be locked out of the
one page that lets them resolve it.
Decoupled frontends (Next.js, React, Angular, Vue, mobile apps)
If your frontend calls the Laravel app as a JSON API rather than navigating full pages
(a Next.js/React/Angular/Vue SPA, or a mobile app), send Accept: application/json
(most HTTP clients โ fetch, axios, etc. โ already do this) and Watchtower responds with
structured JSON instead of an HTML page or an HTTP redirect:
A protected endpoint returns 428 Precondition Required when a challenge is triggered:
// HTTP 428 { "error": "Verification required before this request can proceed.", "code": "CHALLENGE_REQUIRED", "challenge": { "provider": "turnstile", // or "recaptcha" | "hcaptcha" "site_key": "your-site-key", "captcha_configured": true, // false => no CAPTCHA keys set, cooldown mode "cooldown_seconds": 15, "challenge_token": "eyJ...", // opaque, echo this back on verify "verify_url": "https://yourapp.com/_watchtower/challenge/verify" } }
POST the solved CAPTCHA token (or, if captcha_configured is false, just wait
cooldown_seconds and POST anyway) to verify_url as JSON:
// POST /_watchtower/challenge/verify // { "challenge_token": "eyJ...", "captcha_token": "<captcha widget response>" } // 200 OK { "passed": true } // or 422 Unprocessable Entity { "passed": false, "errors": { "captcha": "Verification failed. Please try again." } }
On passed: true, retry your original request โ the identifier is now exempt from being
re-challenged for WATCHTOWER_CHALLENGE_PASS_TTL minutes. This endpoint does not require a
CSRF token or a cookie session: challenge_token is a short-lived, server-signed token
(WATCHTOWER_CHALLENGE_TOKEN_TTL minutes, default 10) that proves the server actually issued
this specific challenge to this specific identifier โ it's what makes the flow work for
stateless clients (Bearer-token auth, a frontend on a different origin, no cookies at all), not
just cookie-based browser sessions.
Two things to set up on your own side for a fully decoupled (cross-origin) deployment:
- If your frontend needs to read Watchtower's response headers (
X-Watchtower-Score,X-Watchtower-Level, etc. โ only sent ifWATCHTOWER_RESPONSE_HEADERS=true) viafetch()/axioson a different origin than your API, add them to your own CORS config (config/cors.php):'exposed_headers' => ['X-Watchtower-Action', 'X-Watchtower-Score', 'X-Watchtower-Level']. The JSON428/429response bodies already carry this same information, so this is optional โ only needed if you specifically want to read it from headers. - CORS preflight (
OPTIONS) requests are automatically exempt from all scoring/rate-limiting, so they don't silently cost your frontend part of its request budget.
Minimal framework-agnostic example (React, Next.js, Angular, or plain JS โ same shape, just swap in your CAPTCHA widget's render call):
async function callProtectedApi(url, options = {}) { const response = await fetch(url, { ...options, headers: { ...options.headers, Accept: 'application/json' }, }); if (response.status !== 428) { return response; // not challenged โ handle normally } const { challenge } = await response.json(); const captchaToken = challenge.captcha_configured ? await renderCaptchaAndWaitForToken(challenge) // your widget's render+resolve call : await new Promise((resolve) => setTimeout(resolve, challenge.cooldown_seconds * 1000)); const verifyResponse = await fetch(challenge.verify_url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ challenge_token: challenge.challenge_token, captcha_token: captchaToken || '', }), }); const { passed } = await verifyResponse.json(); if (!passed) { throw new Error('Challenge verification failed'); } return callProtectedApi(url, options); // retry the original request }
A React hook is just this wrapped in useCallback/state for the CAPTCHA-widget-open UI state โ
the request/response contract above is all a framework binding needs.
Helper Functions
// Get Watchtower instance $watchtower = watchtower(); // Create a protected route watchtower('login')->adaptive()->detectBots(); // Check if identifier is blocked if (watchtower_is_blocked($identifier)) { // Handle blocked user }
๐ Endpoint Profiles
| Profile | Use Case | Base Limit | Bot Detection | Fingerprint | Auto-Harden |
|---|---|---|---|---|---|
default |
General endpoints | 30/min | โ | โ | โ |
critical |
Authentication, PW reset | 10/min | โ | โ | โ |
login |
Login endpoints | 5/min | โ | โ | โ |
api |
API routes | 120/min | โ | โ | โ |
scrape-sensitive |
Search, data endpoints | 30/min | โ | โ | โ |
๐ง Architecture
Incoming Request
โ
Request Analyzer โ Exempt check, honey endpoints
โ
Fingerprint Engine โ Multi-factor device fingerprinting
โ
Behavior Engine โ Timing analysis, entropy, navigation
โ
Threat Scoring System โ Dynamic risk assessment (0-100+)
โ
Adaptive Decision Engine โ Dynamic limit adjustment
โ
Action Layer โ Allow / Slowdown / Challenge / Block / Harden
โ
Response โ Headers, delay, block page, challenge view
๐ Events
Watchtower dispatches events you can listen for:
// EventServiceProvider.php protected $listen = [ \Imran\Watchtower\Events\ThreatDetected::class => [ YourListener::class, ], \Imran\Watchtower\Events\BotDetected::class => [ YourBotListener::class, ], \Imran\Watchtower\Events\AttackPatternDetected::class => [ // Fired for scraper, account-enumeration, distributed-attack, and probing detections YourPatternListener::class, ], \Imran\Watchtower\Events\EndpointHardened::class => [ YourHardeningListener::class, ], ];
Each dispatch is individually toggleable via the events.dispatch_* config keys.
๐ ๏ธ Artisan Commands
# Install Watchtower php artisan watchtower:install # Check status of an identifier php artisan watchtower:status {identifier?} # Unblock an identifier php artisan watchtower:unblock {identifier} # Clear all data for an identifier php artisan watchtower:clear {identifier} # Delete audit-log rows older than privacy.data_retention_days # (self-scheduled daily unless privacy.auto_prune is disabled) php artisan watchtower:prune [--days=N]
๐ฆ Storage
Watchtower supports three storage drivers:
| Driver | Best For | Notes |
|---|---|---|
| Redis ๐ | Production | Fastest, supports lists/expiry for behavioral data |
| Cache | Development | Works with any Laravel cache driver |
| Database | No Redis available | Fast counters (score/requests/violations/etc.) stored in the watchtower_cache table instead |
Independent of which storage driver above is active, logging.log_to_database (on by default)
persists an audit trail โ every scored detection and every block โ to the
watchtower_security_events and watchtower_blocked_entities tables. This is always a real SQL
database connection, separate from the fast-storage driver choice.
Representative sample of the fast-storage keys (driver-prefixed, e.g. watchtower:score:{id}):
requests:{id} โ Request count (sliding window)
score:{id} โ Current threat score
violations:{id} โ Progressive penalty count
blocked:{id} โ Block status with TTL
fingerprint:{id} โ Fingerprint metadata
behavior:{id}:{type} โ Behavioral data
nav:{id} โ Navigation history (list)
timings:{id} โ Request timing data (list)
hardened:{id} โ Auto-hardening status (identifier-level)
hardened_route:{route} โ Auto-hardening status (route-wide)
challenge_passed:{id} โ Verified-challenge exemption
challenge_token issuance/cooldown/snapshot keys for the challenge flow
๐ Privacy
Watchtower respects user privacy with configurable options:
// config/watchtower.php 'privacy' => [ 'anonymize_ip' => true, // Zero the last two IPv4 octets (or last 80 bits of IPv6) // before it's ever hashed, stored, or logged 'hash_fingerprint' => true, // SHA-256 all fingerprint data 'log_full_headers' => false, // Reserved โ no code path logs full headers regardless 'data_retention_days' => 90, // Rows older than this are deleted by watchtower:prune 'auto_prune' => true, // Self-schedule watchtower:prune to run daily ],
๐ Dashboard (Coming in v2)
- Live attack map
- Real-time threat feed
- Blocked users management
- Endpoint heatmaps
- Attack timelines
- Suspicious fingerprint explorer
- Scraping analytics
๐ Integrations (Planned)
- Cloudflare - Sync bans and rules
- Laravel Pulse - Security metrics
- Laravel Telescope - Threat inspection
- Laravel Horizon - Queue protection
- Sanctum / Passport - API token intelligence
๐งช Testing
composer test
โ ๏ธ Important Notes
- Redis is strongly recommended for production. The behavior analysis engine relies on Redis list operations for timing and navigation history.
- False positives are minimized through configurable scoring weights, score decay, and trust levels.
- Performance is optimized with Redis and log sampling (
logging.sample_rate) for high-traffic sites. - Data protection tooling โ anonymized IPs, hashed fingerprints, and enforced data retention (
watchtower:prune) are built in to help with GDPR/privacy compliance; whether your overall deployment is compliant depends on your full data-handling practices, not this package alone.
๐ License
The MIT License (MIT). See LICENSE for more information.
๐๏ธ Built With
- Laravel 10.x โ 13.x
- Redis
- PHP 8.1 โ 8.4
- SHA-256 fingerprinting
- Statistical behavioral analysis