mostafaznv/recaptcha
Laravel integration for Google reCAPTCHA v2 and v3, with simple validation rules, Blade rendering, and config-driven keys. Protect forms and APIs from spam and bots with minimal setup and no boilerplate.
Install and wire it up in under five minutes:
composer require mostafaznv/recaptcha
php artisan vendor:publish --tag=laracaptcha-config
Add the keys to .env (the published config reads from these):
RECAPTCHA_SITE_KEY=your-site-key
RECAPTCHA_SECRET_KEY=your-secret-key
RECAPTCHA_ACTIVE=true
Register the keys for the domains you actually use in the Google reCAPTCHA admin console — v3 keys are domain-scoped, so a key created for example.com will silently return low scores or fail verification on staging.example.com and localhost.
Where to look first: the published config/laracaptcha.php is the source of truth for option names (site key, secret key, active flag, badge position, language). Read it before copying any code from a blog post — this is a small package and option names have shifted between releases. Then look at vendor/mostafaznv/recaptcha/src/ — it is small enough to read in one sitting, and you will need to know exactly how the rule posts to Google when you debug it.
First use case — a contact form. Render the widget inside the form and validate the returned token:
<form id="contact" method="POST" action="{{ route('contact.store') }}">
@csrf
<input name="email" type="email">
<textarea name="message"></textarea>
<button type="submit">Send</button>
{{ LaraCaptcha::render('contact') }}
</form>
use Mostafaznv\LaraCaptcha\Rules\LaraCaptchaRule;
public function store(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'message' => ['required'],
'g-recaptcha-response' => ['required', new LaraCaptchaRule()],
]);
// ...
}
If the form renders but nothing is validated, check the browser network tab for a POST to www.google.com/recaptcha/api/siteverify — if it is absent, the render call never bound to your form (usually an id mismatch).
Render per-form, once per page. The render helper injects a hidden g-recaptcha-response field and the JS that populates it for the form whose id you pass. For pages with multiple forms (login + newsletter on one page), call it once per form id:
{{ LaraCaptcha::render('login') }}
{{ LaraCaptcha::render('newsletter') }}
Do not loop it inside a @foreach of forms without unique ids; the JS binding is id-based.
Validate in a FormRequest, not the controller. The rule is a standard Laravel rule object, so it composes normally:
class ContactRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => ['required', 'email'],
'message' => ['required', 'max:2000'],
'g-recaptcha-response' => ['required', new LaraCaptchaRule()],
];
}
}
Handle AJAX / SPA submissions explicitly. Server-rendered forms are the happy path; for fetch/Axios you must execute the widget yourself and send the token:
const token = await grecaptcha.execute('{{ config('laracaptcha.site_key') }}', { action: 'contact' });
await axios.post('/contact', { ...data, 'g-recaptcha-response': token });
Keep the action string stable per form and (if you add your own verification) assert it server-side — otherwise a token minted for your newsletter form can be replayed against your login form.
Bypass in tests and local dev. Set RECAPTCHA_ACTIVE=false in .env.testing and phpunit.xml, or the rule will make a real HTTP call to Google on every feature test and fail in CI. If you want to assert that verification happens, use Http::fake() and assert the siteverify request was sent rather than hitting the network:
Http::fake([
'www.google.com/recaptcha/api/siteverify' => Http::response(['success' => true, 'score' => 0.9]),
]);
Pair with a score check for sensitive endpoints. The bundled rule answers "is this token valid?". For password resets and registration you usually also want a minimum score. Inspect the verification response yourself (or wrap the rule) and reject anything below your threshold, e.g. 0.5.
success === false alone will not stop scripted abuse.grecaptcha.execute on every submit, including retries.LaraCaptcha::render('contact') bound to <form id="contact-us"> renders and returns a token-less submission. Check the hidden input's value just before submit in DevTools — empty means the binding never fired..env keys run php artisan config:clear. A stale config:cache pointing at your dev site key is the most common "works locally, fails in staging" cause.www. and subdomains) in the reCAPTCHA console.siteverify — it includes error-codes (invalid-input-secret, timeout-or-duplicate, invalid-input-response), which tell you immediately whether it is a key problem, a replayed token, or a bad token.composer.json and be prepared to swap the validation rule for ~15 lines of your own Http::post to siteverify if you hit a Laravel/PHP incompatibility. The render helper is thin sugar over Google's documented JS snippet anyway.How can I help you explore Laravel packages today?