imran/laravel-watchtower

Intelligent Adaptive Rate Limiter & Security Shield for Laravel - Advanced threat detection, behavioral analysis, fingerprinting, and auto-hardening protection.

Maintainers

Package info

github.com/grim-reapper/laravel-watchtower

pkg:composer/imran/laravel-watchtower

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-18 18:03 UTC

This package is auto-updated.

Last update: 2026-07-18 18:11:53 UTC


README

Tests License

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 endpoints
  • penalties - Progressive penalty levels and durations
  • adaptive - Dynamic limit adjustment settings
  • threat_scoring - Score thresholds and decay settings
  • behavior - Behavioral analysis configuration
  • bot_detection - Bot detection rules and patterns
  • auto_hardening - Auto-hardening behavior
  • honey_endpoints - Fake endpoints for bot trapping
  • fingerprint - Fingerprinting components
  • exempt - 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:

  1. If your frontend needs to read Watchtower's response headers (X-Watchtower-Score, X-Watchtower-Level, etc. โ€” only sent if WATCHTOWER_RESPONSE_HEADERS=true) via fetch()/ axios on 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 JSON 428/429 response bodies already carry this same information, so this is optional โ€” only needed if you specifically want to read it from headers.
  2. 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

  1. Redis is strongly recommended for production. The behavior analysis engine relies on Redis list operations for timing and navigation history.
  2. False positives are minimized through configurable scoring weights, score decay, and trust levels.
  3. Performance is optimized with Redis and log sampling (logging.sample_rate) for high-traffic sites.
  4. 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