dominservice/invis-captcha

Invisible, score-based CAPTCHA for Laravel.

1.1.7 2025-08-20 07:53 UTC

This package is auto-updated.

Last update: 2025-08-20 07:53:48 UTC


README

Latest Version on Packagist Total Downloads License

A zero-UI, score-based anti-bot shield (reCAPTCHA v3-style) with optional honey-field, 1-px pixel, polyfill-poisoning, dynamic field names, ML scoring and Cloudflare Turnstile fallback.

Version Matrix

Laravel Supported? Notes
9.x Requires PHP ≥ 8.1
10.x Classic “Kernel + app/Http” structure
11.x New streamlined structure (no Kernel by default)
12.x Identical to 11 — tested with 12.0.0-beta

If you are upgrading an older app to 11/12 you may still keep the classic structure – follow the ≤10 instructions.

✨ Features

Module Purpose Toggle
Invisible scoring JS collects signals → server returns JWT with score ∈ [0-1]. always on
Dynamic field names Adds random suffixes (e.g. email_d8e7f3c1) to fool static parsers. dynamic_fields.enabled
Honey field Hidden input ― if filled → instant block. honey_field.enabled
1-px tracking pixel Logs real browsers vs. lazy headless fetches. track_pixel.enabled
Polyfill-Poisoning Patches browser APIs (e.g. Canvas.toDataURL()) to break fingerprint-spoofers. polyfill_poison.enabled
Cloudflare Turnstile fallback Shows Turnstile widget only for low scores. turnstile.enabled
Pluggable ML model Drop-in JSON model for advanced scoring. ml_model.enabled

Installation

You can install the package via composer:

composer require dominservice/invis-captcha

After installing, publish the configuration file:

php artisan vendor:publish --tag="invis"
# generates default thresholds model – only when file doesn't exist
php artisan invis:model:generate

# forces overwrite
php artisan invis:model:generate thresholds --force

# variant with linear weights
php artisan invis:model:generate weights

Configuration

The configuration file config/invis.php allows you to customize:

  • Secret key for JWT tokens
  • Score threshold for bot detection
  • Honeypot field settings
  • Dynamic field name generation
  • Cloudflare Turnstile integration
  • Tracking pixel options

Toggle any module with true / false:

Framework-specific wiring

Laravel ≤ 10 (classic structure)

Register middleware alias

// app/Http/Kernel.php   (inside the $routeMiddleware array)
'verify.invis' => \Dominservice\Invisible\Middleware\Verify::class,

Protect a route

Route::post('/contact', ContactController::class)
     ->middleware('verify.invis');

Laravel ≥ 11 (streamlined structure)

Laravel 11+ uses bootstrap-driven configuration.

// bootstrap/app.php  (excerpt)

use Illuminate\Foundation\Configuration\Middleware;
use Dominservice\Invisible\Middleware\Verify;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        // register as ALIAS
        $middleware->alias([
            'verify.invis' => Verify::class,
        ]);
    })
    ->withRoutes(function () {
        require __DIR__.'/../routes/web.php';
    })
    ->create();

Protect routes exactly the same way in routes/web.php:

Route::post('/contact', ContactController::class)
     ->middleware('verify.invis');

Basic Usage

1. Add the Blade directive to your form

<form method="POST" action="/submit">
    @csrf
    @invisCaptcha
    
    <!-- Your form fields -->
    <input type="text" name="name">
    <input type="email" name="email">
    
    <button type="submit">Submit</button>
</form>

2. Protect your routes with the middleware

// In a route file
Route::post('/submit', 'FormController@submit')->middleware('verify.invis');

// Or in a controller
public function __construct()
{
    $this->middleware('verify.invis');
}

How It Works

  1. The @invisCaptcha directive adds JavaScript that collects user behavior data
  2. When the form is submitted, a score is calculated based on:
    • Mouse movements
    • Keyboard usage
    • Time spent on page
    • Other behavioral signals
  3. A JWT token with the score is sent with the form
  4. The middleware validates the token and rejects suspicious submissions

Advanced Usage

Custom Score Threshold

You can specify a custom score threshold for specific routes:

Route::post('/contact', 'ContactController@submit')
    ->middleware('verify.invis:0.7'); // Higher threshold for stricter protection

JavaScript Form Submissions

To use the invisible captcha with JavaScript/AJAX form submissions:

  1. Add the Blade directive to your page (outside the form):
@invisCaptcha
  1. Add the data-invis attribute to your form:
<form id="myForm" data-invis>
    <!-- Your form fields -->
</form>
  1. In your JavaScript, wait for the token to be injected before submitting:

Livewire Form Integration

To use the invisible captcha with Livewire forms:

  1. Add the Livewire-specific Blade directive to your main layout file (not inside Livewire components):
<!-- In resources/views/layouts/app.blade.php or your main layout file -->
<!DOCTYPE html>
<html>
<head>
    <!-- ... other head elements ... -->
    <title>Your App</title>
    @livewireStyles
</head>
<body>
    <!-- ... your layout content ... -->
    
    {{ $slot ?? $content ?? yield('content') }}
    
    @livewireScripts
    @invisLivewire  <!-- Place this AFTER @livewireScripts -->
</body>
</html>
  1. Important placement notes:

    • The @invisLivewire directive must be placed in your main layout file, not in individual Livewire component views
    • It should be placed after @livewireScripts to ensure Livewire is loaded first
    • You only need to include it once in your main layout
  2. Your Livewire forms will be automatically detected and protected. The directive:

    • Automatically adds the data-invis attribute to Livewire forms
    • Handles dynamic form updates through Livewire
    • Re-initializes protection after Livewire updates
  3. No additional configuration is needed in your Livewire components

  4. Ensure your Livewire component's form submission method is protected with the middleware:

// In your controller that handles the Livewire form submission
public function submit()
{
    // This method needs to be protected with the middleware
    $this->middleware('verify.invis');
    
    // Your form processing logic
}

Troubleshooting Livewire Integration

If the Livewire integration is not working:

  1. Make sure the @invisLivewire directive is placed in your main layout file, not in individual Livewire components
  2. Verify that it comes AFTER @livewireScripts in your HTML
  3. Check your browser console for any JavaScript errors
  4. Verify that your forms have Livewire attributes (wire:submit, wire:model, etc.)
  5. Make sure you've published the package assets: php artisan vendor:publish --tag="invis"
  6. Check that the middleware is properly registered and applied to your form submission handler
document.getElementById('submitButton').addEventListener('click', async function(e) {
    e.preventDefault();
    
    // Wait for the token to be injected (if not already)
    if (!document.querySelector('input[name="invis_token"]')) {
        await new Promise(resolve => setTimeout(resolve, 500));
    }
    
    const form = document.getElementById('myForm');
    const formData = new FormData(form);
    
    // Send with fetch
    fetch('/your-endpoint', {
        method: 'POST',
        body: formData,
        credentials: 'same-origin'
    })
    .then(response => response.json())
    .then(data => {
        // Handle response
    })
    .catch(error => {
        // Handle error
    });
    
    // Or with axios
    // axios.post('/your-endpoint', formData)
    //     .then(response => { /* Handle response */ })
    //     .catch(error => { /* Handle error */ });
});
  1. Ensure your endpoint is protected with the middleware:
Route::post('/your-endpoint', 'YourController@handle')
    ->middleware('verify.invis');

Cloudflare Turnstile Integration

Enable Turnstile in your config file and add your site and secret keys:

'turnstile' => [
    'enabled' => true,
    'sitekey' => 'your-site-key',
    'secret' => 'your-secret-key',
    'fallback' => 0.30,
],

Translations

The package includes translations for error messages in English and Polish. You can publish the translation files to customize them:

php artisan vendor:publish --tag="invis-translations"

This will publish the translation files to resources/lang/vendor/invis/ where you can edit them or add new languages.

Available Error Messages

The following error messages are available for translation:

  • honey_field - Displayed when a bot fills the honey field
  • missing_token - Displayed when the token is missing from the request
  • invalid_token - Displayed when the token is invalid
  • token_expired - Displayed when the token has expired
  • invalid_signature - Displayed when the token signature is invalid
  • ip_mismatch - Displayed when the IP address doesn't match
  • score_too_low - Displayed when the score is below the threshold
  • turnstile_error - Displayed when there's an error with Turnstile verification

Adding a New Language

To add a new language, create a new directory in resources/lang/vendor/invis/ with your language code (e.g., de for German) and copy the structure from the English files.

Testing

composer test

Security

If you discover any security related issues, please email biuro@dso.biz.pl instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.

Credits