buzz/laravel-google-captcha
Laravel integration for Google reCAPTCHA v2 and v3, adding validation rules, Blade directives and helpers to render and verify captchas. Configurable site/secret keys, score thresholds and error messages help protect forms from spam and bots with minimal setup.
Install via Composer and let package discovery do its job on Laravel 5.5+:
composer require buzz/laravel-google-captcha
Publish the config file (the provider class is Buzz\LaravelGoogleCaptcha\CaptchaServiceProvider):
php artisan vendor:publish --provider="Buzz\LaravelGoogleCaptcha\CaptchaServiceProvider" --tag=config
Add your Google reCAPTCHA credentials to .env. Grab them from https://www.google.com/recaptcha/admin — this is the first place to look if anything silently fails, because the package does no sanity checking on missing keys:
CAPTCHA_SITEKEY=your-site-key
CAPTCHA_SECRET=your-secret-key
The canonical first use case is a contact form. In the Blade view:
<form method="POST" action="/contact">
@csrf
{!! Captcha::render() !!}
<button type="submit">Send</button>
</form>
In the controller (or better, a FormRequest):
$this->validate($request, [
'g-recaptcha-response' => 'required|captcha',
]);
Note the rule is attached to the g-recaptcha-response field — not to a field named captcha. That single line is where most first attempts break.
FormRequest encapsulation. The rule is a standard Laravel validation rule, so push it into a FormRequest and keep controllers clean:
class ContactRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string',
'email' => 'required|email',
'message' => 'required|string',
'g-recaptcha-response' => 'required|captcha',
];
}
public function messages(): array
{
return [
'g-recaptcha-response.required' => 'Please complete the captcha.',
'g-recaptcha-response.captcha' => 'Captcha verification failed, try again.',
];
}
}
Middleware for route groups. If multiple public forms need protection, wrap the rule in a middleware that inspects g-recaptcha-response and throws a ValidationException — this keeps controllers and requests unchanged:
class VerifyCaptcha
{
public function handle($request, Closure $next)
{
validator(
['g-recaptcha-response' => $request->input('g-recaptcha-response')],
['g-recaptcha-response' => 'required|captcha']
)->validate();
return $next($request);
}
}
Then Route::middleware(VerifyCaptcha::class)->group(...) around the public POST routes.
Manual verification with the facade. When the rule doesn't fit — e.g. an Ajax endpoint returning JSON, or you want the failure reason — call the verifier directly. The facade is Buzz\LaravelGoogleCaptcha\Facades\Captcha:
if (! Captcha::verify(request('g-recaptcha-response'))) {
return response()->json(['error' => 'captcha_failed'], 422);
}
This is the pattern to reach for when you want to log failure codes or gate on a v3 score threshold before accepting the request.
Blade rendering options. Captcha::render() reads the attributes array from config/captcha.php. To theme a widget differently on one page, override inline rather than editing the shared config:
{!! Captcha::render() !!} {{-- uses config defaults --}}
{{-- or push a different theme for a single form --}}
{!! Captcha::render(['theme' => 'dark']) !!}
v3 / score-based flows. The wrapper is a thin server-side verifier. For v3 you must (a) include the Google JS with your site key, (b) execute it with an action name on the client, and (c) decide on a score threshold on the server. The package handles only the final siteverify call — score checks, action matching, and the JS wiring are on you. For most day-to-day forms, stick with v2 checkbox and let the rule do everything.
Testing. Because the rule makes an HTTP call, fake it in feature tests rather than hitting Google:
Http::fake([
'https://www.google.com/recaptcha/api/siteverify*' => Http::response([
'success' => true,
], 200),
]);
Or use Google's documented public test keys in .env.testing and skip the fake, since Google explicitly accepts them.
captcha is the rule; g-recaptcha-response is the field. Misreading this is the number-one source of "validation always fails" tickets..env keys produce silent failures, not exceptions. The package will happily call Google with an empty secret and Google returns {"success": false}. If the rule always fails, check config('captcha.secret') first..env or config/captcha.php, run php artisan config:clear (and config:cache in production). If the site key in the rendered widget doesn't match your dashboard, this is usually why.localhost reCAPTCHA v2 works but v3 score thresholds can reject you aggressively because Google can't fingerprint the environment. Use the Google test keys (6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI site / 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe secret) locally — they always pass and never call the real service.grecaptcha.reset().verify => true. Behind a corporate proxy or in a Docker network without CA certs you'll get a RequestException rather than a validation failure — catch it separately or set CAPTCHA_VERIFY=false (via the options array) for local containers, and never in production.siteverify response includes error-codes (e.g. invalid-input-secret, timeout-or-duplicate), but the validation rule collapses everything into a single boolean. If you need to distinguish "bad secret" from "user token expired" in logs, do the manual Captcha::verify() call and inspect the response yourself.Captcha::render() works out of the box. On Laravel ≤5.4 you must register Buzz\LaravelGoogleCaptcha\CaptchaServiceProvider and alias Buzz\LaravelGoogleCaptcha\Facades\Captcha as Captcha in config/app.php, otherwise you get a Class 'Captcha' not found in Blade.options (timeout, headers, verify). It does not expose a pluggable verifier or event hooks — if you need enterprise reCAPTCHA or a custom proxy endpoint, fork the small Captcha class rather than fighting the config. With only ~200 stars and no downstream dependents, expect to own fixes yourself.CAPTCHA_SITEKEY belongs client-side. If CAPTCHA_SECRET shows up in view source, rotate it in the Google admin console immediately.Captcha facade (Captcha::shouldReceive('verify')->andReturn(true)), remember the validation rule calls the facade internally — so a facade partial mock is enough to bypass the network in unit tests without Http::fake.composer why-not buzz/laravel-google-captcha after major Laravel upgrades; if a constraint blocks you, the ~5 files in src/ are small enough to vendor.How can I help you explore Laravel packages today?