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:
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 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:
- 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.