Captcha by asign
CommunityOne captcha field for forms, the panel login and Livewire: reCAPTCHA v2/v3, Cloudflare Turnstile and hCaptcha, checked on the server.
Author:
asign
Package health
Automated checks of this plugin's Composer package
15 checks
- Passed: GitHub Actions pinned to SHA
- Skipped: GitLab CI includes pinned to SHA
- Passed: Open security advisories
- Passed: Dependabot PR responsiveness — No open Dependabot PRs.
- Skipped: Renovate MR responsiveness
- Passed: Dependabot or Renovate configured
- Passed: Dependency update cooldown configured
- Passed: Provides a security policy
- Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
- Passed: Commit and release recency — Active: last commit 0 days ago; last release 0 days ago.
-
Passed:
composer.lock not committed by library
—
composer.lockis absent from the released dist archive. - Passed: Dist archive is lean
-
Passed:
Current Laravel version supported
—
Package dependencies resolve together with current Laravel
13.0. -
Passed:
Current PHP version supported
—
Constraint
^8.3supports current PHP8.5. - Skipped: Current Symfony version supported
filament/
namespace. Review the source and install at your own risk. Found
malware or an unresolved security issue the author won't
address?
Report it
.
Documentation
- Screenshots
- Requirements
- Installation
- Usage
- Configuration
- Content-Security-Policy
- Gotchas
- Translations
- AI agents
- Testing
- Changelog
- License
A captcha field for Filament 5 forms and plain Livewire components. One field, one validation rule, five providers: Google reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3 (score and action), Cloudflare Turnstile and hCaptcha. It re-initialises after Livewire morphs, resets the single-use token after every failed submit, fetches v3 tokens right before the form goes out, and has a fake mode so your test suite never touches the network.
#Screenshots
The login page with the Turnstile widget (the same field works on registration and password reset):

A missing or failed check is reported under the widget and the single-use token is reset:


Captcha::make() in an ordinary Filament form:

Dark mode:

#Requirements
- PHP 8.3+
- Laravel 12 or 13
- Filament 5 (which brings Livewire 4; the plain-Livewire widget uses the same)
#Installation
composer require asignua/filament-captcha
php artisan vendor:publish --tag=filament-captcha-config # optional
Put the keys of the provider you use in .env (see Configuration):
CAPTCHA_DRIVER=turnstile
CAPTCHA_TURNSTILE_SITE_KEY=...
CAPTCHA_TURNSTILE_SECRET_KEY=...
Register the plugin on the panel only if you want the captcha on Filament's auth pages, or want the client script printed on every panel page automatically (the default):
use Asignua\FilamentCaptcha\CaptchaPlugin;
$panel
->login()
->plugin(CaptchaPlugin::make()->login()->registration()->passwordReset());
Call ->plugin() after ->login() / ->registration() / ->passwordReset(): the plugin swaps in page classes that
add the field, and a later call on the panel would swap them back.
No filament:assets step: the small client script is served by the package itself (/filament-captcha/captcha.js,
versioned and cacheable for a year).
#Usage
#In a Filament form
use Asignua\FilamentCaptcha\Forms\Components\Captcha;
$schema->components([
TextInput::make('email')->email()->required(),
Captcha::make(), // default driver from config
]);
Captcha::make('captcha')
->driver('recaptcha_v3')
->captchaAction('contact') // v3 / Turnstile action name
->minScore(0.7)
->hostnames(['example.com'])
->theme('dark') // auto (default) | light | dark
->locale('uk');
The field is validated, never saved: it is not dehydrated, so the single-use token does not end up in $form->getState().
It is named captchaAction() and not action() because Filament's own Component::action() is taken.
#In a plain Livewire component
Once in your layout (Filament panels get it from the plugin):
<x-filament-captcha::scripts />
use Asignua\FilamentCaptcha\Concerns\InteractsWithCaptcha;
class ContactForm extends Component
{
use InteractsWithCaptcha; // public string $captchaToken
public function submit(): void
{
$this->validateCaptcha(action: 'contact'); // throws ValidationException, resets the widget either way
// ...
}
}
<form wire:submit="submit">
...
<x-filament-captcha::widget wire:model="captchaToken" action="contact" />
@error('captchaToken') <p>{{ $message }}</p> @enderror
<button type="submit">Send</button>
</form>
The widget also takes driver, theme, size and locale. Outside Livewire (no wire:model) it renders a hidden input
named captcha_token (name="..." to change it).
#As a validation rule
use Asignua\FilamentCaptcha\Facades\Captcha;
$request->validate([
'captcha' => [Captcha::rule()->action('contact')->minScore(0.7)],
]);
The rule is implicit: an empty or missing token fails with "Please complete the security check", it does not skip the rule. Within one request the same token is verified once, so repeated validation does not burn it.
#Auth pages
CaptchaPlugin::make()->login()->registration()->passwordReset() adds the field to Filament's pages. Pass a driver per page
(->login(driver: 'recaptcha_v3'); the action is login, register or password_reset). With your own page classes use the
trait instead:
class Login extends \Filament\Auth\Pages\Login
{
use \Asignua\FilamentCaptcha\Concerns\AddsCaptchaField;
protected function captchaScope(): string { return 'login'; }
}
The trait works without registering the plugin (the driver then comes from config/filament-captcha.php), but the page
still needs the client script: add <x-filament-captcha::scripts /> through a panel render hook (for example
PanelsRenderHook::HEAD_END).
Multi-factor login: Filament runs authenticate() twice (password, then the challenge). The captcha is checked on the
first step only; on the challenge step the field is hidden, because the token was spent. This works for the plugin's pages
and for the trait alike.
#Tests
use Asignua\FilamentCaptcha\Facades\Captcha;
Captcha::fake(); // widget not drawn, nothing sent, validation passes
Captcha::fake(passes: false); // validation fails
Or set CAPTCHA_FAKE=true in .env.testing (honoured only in local / testing; elsewhere it is ignored with a log warning). Without a call to fake(), use Http::fake() for the provider's siteverify
URL: the package resolves the HTTP client on every verification, so it honours your fake.
#Configuration
config/filament-captcha.php, all of it also reachable through .env:
| Key | Default | |
|---|---|---|
driver (CAPTCHA_DRIVER) |
turnstile |
recaptcha_v2, recaptcha_v2_invisible, recaptcha_v3, turnstile, hcaptcha |
enabled (CAPTCHA_ENABLED) |
null |
false turns the captcha off everywhere |
fake (CAPTCHA_FAKE) |
false |
no widget, no network, validation passes |
missing_keys (CAPTCHA_MISSING_KEYS) |
auto |
keys missing: auto = off with a log warning in local/testing, fails elsewhere; disable; fail |
timeout (CAPTCHA_TIMEOUT) |
5 |
seconds to wait for the provider |
fail_open (CAPTCHA_FAIL_OPEN) |
false |
let visitors through when the provider is unreachable (a rejected token is never let through) |
hostnames |
null |
null = no check, true = the request host, or an allow-list |
theme |
auto |
auto follows the dark class on <html> |
token_refresh |
100 |
seconds between v3 token refreshes |
script_path |
filament-captcha/captcha.js |
null to serve resources/js/captcha.js yourself |
drivers.* |
site_key, secret_key per driver; reCAPTCHA domain (www.recaptcha.net), v3 score_threshold and default action, hCaptcha size (invisible) |
reCAPTCHA v2 checkbox and v2 invisible need separate key pairs (Google issues a different key type for each).
#Custom provider
Captcha::extend('mine', fn (array $config, Factory $http) => new MyDriver($config, $http));
Implement Asignua\FilamentCaptcha\Contracts\CaptchaDriver (or extend Drivers\SiteVerifyDriver: set the endpoint and
clientConfig(); the client script knows the recaptcha, turnstile and hcaptcha kinds).
#Content-Security-Policy
The widget needs the provider's origins. Captcha::cspSources(['turnstile']) returns them by directive (script-src,
frame-src, connect-src, and style-src for hCaptcha). Add them to your policy. With
asignua/filament-csp-nonce the directives() you pass are merged over the
preset directive by directive, so include the sources the preset already has:
CspNoncePlugin::make()->directives([
'frame-src' => ["'self'", ...Captcha::cspSources()['frame-src']],
'connect-src' => ["'self'", ...Captcha::cspSources()['connect-src']],
]);
The package prints <script src nonce="..."> with Vite::cspNonce() and the client script gives the same nonce to the
provider's script tag it injects. With 'strict-dynamic' that is enough; without it, allow the provider origins in script-src.
#Gotchas
- A token is single-use. The widget resets after every Livewire request that carried it, so a failed login makes the
visitor tick the box again. A request that carries the token without verifying it (a
live()field in the same form) also resets it. Only a request that called a method (a submit) resets the widget, solive()field updates do not undo a solved challenge. The reset event fromresetCaptcha()is scoped to the Livewire component, so two forms on one page do not clear each other. - Never hide the field with
->visible()/->hidden()(the multi-factor step above is the one built-in exception). Hidden Filament fields are not validated, so that would open the form to bots. The field already draws nothing in fake / disabled mode (where the rule passes anyway) and draws an error in misconfigured mode (where the rule fails). - Missing keys in production fail the form, on purpose. In
localandtestingthey disable the captcha with a warning in the log; look for "DISABLED" there before wondering why a form accepts bots. - v3
actionmust match. The widget asks Google for the action, the rule checks it comes back. Both default to the driver'saction(submit); name it once on the field or pass the same string to<x-filament-captcha::widget action>andvalidateCaptcha(action:). - reCAPTCHA v2 and v3 on one page do not mix (one global
grecaptcha). - Load the script before Alpine. The plugin does it for panels; in your own layout put
<x-filament-captcha::scripts />in the<head>or before@livewireScripts. - reCAPTCHA v3 badge. If you hide it with CSS, Google requires the notice text in your form.
#Translations
The interface ships in English, Ukrainian, German, Spanish, French, Italian, Dutch, Polish, Brazilian Portuguese and Turkish
under the filament-captcha::filament-captcha namespace. A test keeps every language in step with the English keys. Override a
string by publishing the translations (--tag=filament-captcha-translations) and editing the copy in
lang/vendor/filament-captcha.
#AI agents
The package ships Laravel Boost guidelines (resources/boost/guidelines/core.blade.php) so a
coding agent wires it up correctly.
#Testing
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
#Changelog
See CHANGELOG.md.
#License
The MIT License (MIT). See LICENSE.md.
The author
asign is a small web-dev company from Lviv, Ukraine. We build business applications on Laravel and Filament — CRMs, automation systems for standard and non-standard business processes, booking and content management systems, including our own Filament-based CMS. We open-source the parts that prove useful beyond a single project
From the same author
SEO Files
Generate and edit sitemap.xml with hreflang and a sitemap index for large sites, robots.txt and llms.txt / llms-full.txt from your Filament panel
Author:
asign
Mail Log
Log of every outgoing mail with queue status, a sandboxed preview and signed links, tokens and passwords redacted before storage.
Author:
asign
Clusters
Group several fields into one input row with a shared label, joined borders and one focus ring — the successor of guava/filament-clusters.
Author:
asign
Toggle Icon Column
Clickable icon column that toggles a boolean in place, with on/off colours and hover preview — the successor of archilex's plugin.
Author:
asign
Featured Plugins
A selection of plugins curated by the Filament team
Sharp Theme
A theme that gives panels a precise, technical look with square corners, strong borders, and clear contrast.
Filament
Advanced Tables (formerly Filter Sets)
Supercharge your tables with powerful features like user-customizable views, quick filters, multi-column sorting, advanced table searching, convenient view management, and more. Compatible with Resource Panel Tables, Relation Managers, Table Widgets, and Table Builder!
Kenneth Sese
Soft Theme
A theme that gives panels a warm, approachable look with rounded shapes, gentle colors, and serif headings.
Filament