dominservice/invis-captcha

Invisible, score-based CAPTCHA for Laravel.

1.1.8 2025-08-27 18:23 UTC

This package is auto-updated.

Last update: 2025-08-27 18:24:15 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:

Direct Function Call

You can also call the invisCaptcha function directly at any time:

// Call with default configuration
window.invisCaptcha().then(result => {
    console.log('Token:', result.token);
    console.log('Score:', result.score);
});

// Or with custom configuration
window.invisCaptcha({
    // Custom configuration options
}).then(result => {
    // Use the token and score
    const { token, score } = result;
    
    // Add token to your form or request
    document.querySelector('input[name="invis_token"]').value = token;
});

This is useful for:

  • Single-page applications
  • Dynamic form creation
  • Custom validation flows
  • Refreshing tokens without page reload

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 blade 
div x-data="{ root: null, mo: null }"
                             x-init="
        root = $el;
        mo = new MutationObserver((mutations) => {
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (!(node instanceof HTMLElement)) continue;

                    const inp = node.matches?.('input[type=hidden][name=invis_token]')
                        ? node
                        : node.querySelector?.('input[type=hidden][name=invis_token]');

                    if (inp) {
                        $wire.set('invis_token', inp.value);
                        inp.addEventListener('input',  () => $wire.set('invis_token', inp.value));
                        inp.addEventListener('change', () => $wire.set('invis_token', inp.value));
                    }
                }
            }
        });
        mo.observe(root, { childList: true, subtree: true });
     ">
                        <form wire:submit.prevent="submitReport" data-invis>


// in component
// In your controller that handles the Livewire form submission
public function submit()
{
    // This method needs to be protected with the middleware
        try {
            app(\Dominservice\Invisible\Middleware\Verify::class)
                ->handle(request(), fn () => true);
        } catch (HttpException $e) {
            $this->showVerificationError = true;
        }
    
    // Your form processing logic
}

Note: The middleware automatically detects and processes Livewire requests. It will look for tokens and form fields in both regular POST data and Livewire component data. The middleware supports both the standard Livewire format and the newer format with snapshot (JSON string) and updates fields. This means you don't need any special configuration to use the middleware with Livewire forms - it just works!

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