dominservice / invis-captcha
Invisible, score-based CAPTCHA for Laravel.
Requires
- php: ^8.1
- firebase/php-jwt: ^6.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0 || ^12.0
Requires (Dev)
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.0
- phpunit/phpunit: ^10.0
README
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
- The
@invisCaptcha
directive adds JavaScript that collects user behavior data - When the form is submitted, a score is calculated based on:
- Mouse movements
- Keyboard usage
- Time spent on page
- Other behavioral signals
- A JWT token with the score is sent with the form
- 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:
- Add the Blade directive to your page (outside the form):
@invisCaptcha
- Add the
data-invis
attribute to your form:
<form id="myForm" data-invis> <!-- Your form fields --> </form>
- 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:
- 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>
-
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
- The
-
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
- Automatically adds the
-
No additional configuration is needed in your Livewire components
-
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) andupdates
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:
- Make sure the
@invisLivewire
directive is placed in your main layout file, not in individual Livewire components - Verify that it comes AFTER
@livewireScripts
in your HTML - Check your browser console for any JavaScript errors
- Verify that your forms have Livewire attributes (wire:submit, wire:model, etc.)
- Make sure you've published the package assets:
php artisan vendor:publish --tag="invis"
- 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 */ }); });
- 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 fieldmissing_token
- Displayed when the token is missing from the requestinvalid_token
- Displayed when the token is invalidtoken_expired
- Displayed when the token has expiredinvalid_signature
- Displayed when the token signature is invalidip_mismatch
- Displayed when the IP address doesn't matchscore_too_low
- Displayed when the score is below the thresholdturnstile_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.