albertcht/invisible-recaptcha
A Laravel package for Google Invisible reCAPTCHA that renders the widget and validates submissions server-side with minimal setup. It helps protect forms from spam and bots without adding a visible checkbox for users, integrating cleanly with Laravel validation rules.
Install and wire it up (Laravel).
composer require albertcht/invisible-recaptcha
php artisan vendor:publish --provider="Albertcht\InvisibleRecaptcha\InvisibleRecaptchaServiceProvider"
Laravel 5.5+ auto-discovers Albertcht\InvisibleRecaptcha\InvisibleRecaptchaServiceProvider and registers the InvisibleRecaptcha facade. On older versions, add the provider to config/app.php and alias the facade (Albertcht\InvisibleRecaptcha\Facades\InvisibleRecaptcha).
Get keys. Register the site at https://www.google.com/recaptcha/admin, choose the Invisible reCAPTCHA type, and add every hostname you serve from — including localhost if you want real keys in development. Put the pair into .env using the names referenced by the published config/invisible-recaptcha.php (commonly INVISIBLE_RECAPTCHA_SITE_KEY / INVISIBLE_RECAPTCHA_SECRET_KEY; if the names differ, read them straight from config('invisible-recaptcha.…')).
First use case: a contact form.
Blade:
<form method="POST" action="/contact">
@csrf
...
{!! InvisibleRecaptcha::render() !!}
<button type="submit">Send</button>
</form>
Route:
Route::post('/contact', [ContactController::class, 'store'])
->middleware('invisible-recaptcha');
The invisible-recaptcha middleware is the shortest path: it reads g-recaptcha-response from the request, calls Google's siteverify, and rejects the request when verification fails.
Where to look first. The README's Laravel section, then the published config file (site/secret keys, skip_ips, verify options), then src/Middleware to see exactly how a failure is surfaced — exception, redirect-with-errors, or 422. For non-Laravel projects the README carries separate Lumen, CodeIgniter, and native-PHP bootstrap snippets; the Laravel provider is just sugar over the same verifier.
Route middleware (default). Best for ordinary server-rendered forms. Attach it only to the POST/state-changing routes that render the widget — do not blanket-apply it to a route group, because GET requests have no token and will fail.
Controller-level verification. Use this when you need a custom error shape (JSON for AJAX, a specific redirect, logging):
use Albertcht\InvisibleRecaptcha\Facades\InvisibleRecaptcha;
public function store(Request $request)
{
$ok = InvisibleRecaptcha::verify(
$request->input('g-recaptcha-response'),
$request->ip()
);
if (! $ok) {
return back()->withErrors(['captcha' => 'Verification failed, please retry.']);
}
// ...
}
Some versions expose a convenience verifyRequest($request); check the facade source in src/Facades for the exact method set in your installed version.
Validation-rule style. If you prefer Form Requests, wrap the facade call in a custom rule (Rule::captcha()), or use whatever validation rule the package ships — confirm in the README before relying on it. A custom rule keeps your controller clean and gives you messages() for free.
AJAX / SPA. Render the invisible widget, then trigger it explicitly from JS and send the token yourself:
grecaptcha.execute(); // v2 invisible
// or, for v3-style keys:
grecaptcha.execute(SITE_KEY, { action: 'contact' })
.then(({ token }) => fetch('/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({ ..., 'g-recaptcha-response': token })
}));
The field name must stay g-recaptcha-response unless you also configure the package to look elsewhere.
Multiple forms per page. Each render() call needs a unique data-callback (and, if you use it, data-bind), otherwise the widgets share a callback and only one form ever receives a token. Pass the callback name through the render options per form.
Testing. Use Google's public test keys — site 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI, secret 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe — which always pass and are never blocked. For pure unit tests, $this->withoutMiddleware() or a container swap of the verifier is faster than hitting Google.
Local dev and CI. The skip_ips setting lets you whitelist known IPs (load generators, e2e runners). Never leave it populated in production config.
Lumen / native PHP. Lumen has no auto-discovery: register the provider in bootstrap/app.php and add $app->configure('invisible-recaptcha'). Native PHP: bootstrap the package as the README's "native" section shows, then call the same verify method with the posted token and remote IP.
Extension points. Swap the verifier binding in the container for your own implementation, subclass the middleware to change the failure response, or override the config. All three are simpler than forking.
timeout-or-duplicate and valid users get rejected.grecaptcha.execute(). A <button type="submit"> that posts directly, or a form.submit() call outside the callback, sends an empty g-recaptcha-response. Wire data-callback (or data-bind) and let Google call you.https://www.google.com/recaptcha/api.js must be on the page. If the package's render() doesn't inject it in your setup, add it once, async defer.script-src https://www.google.com https://www.gstatic.com and frame-src https://www.google.com, or the widget never renders and the form never submits.$request->ip() can be your load balancer's address. Configure TrustProxies before passing it as remoteip, or omit the IP entirely; Google treats it as optional.data-sitekey belongs in HTML. Leaking the secret means anyone can forge verifications against your quota.missing-input-secret, invalid-input-secret, missing-input-response, invalid-input-response, bad-request, timeout-or-duplicate. Decide whether an API outage is fail-open (let users through) or fail-closed (block, queue, retry) — the package won't decide for you.php artisan config:cache after changing env vars. Stale cached config pointing at the wrong key is a classic "it works on my machine" cause.score/action fields unless your site key and the package version actually support v3.illuminate/* constraint in the package's composer.json accepts your Laravel major; on Laravel 10/11 you may need a fork, a Composer version override, or a different package. Test the middleware path on your actual framework version before committing to it.secret non-empty? domain registered? outbound HTTPS to www.google.com allowed? CSP clean? middleware attached to the POST route and not also verifying a consumed token? That sequence resolves most failures.How can I help you explore Laravel packages today?