Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Recaptcha Laravel Package

biscolab/laravel-recaptcha

Laravel package for Google reCAPTCHA v2 and v3 integration, with support for multiple captcha types, validation rules, and easy configuration for protecting forms from spam and bots.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require biscolab/laravel-recaptcha
php artisan vendor:publish --provider="Biscolab\ReCaptcha\ReCaptchaServiceProvider" --tag=config

The service provider is auto-discovered; there is no manual registration step on modern Laravel. After publishing, config/recaptcha.php is the single source of truth — read it before anything else. The next files to open are the ReCaptcha class itself (Biscolab\ReCaptcha\ReCaptcha) and the service provider, which is where the recaptcha validator rule and the Blade directives are registered. That is the whole package surface area; it is small.

Minimum viable configuration

Set the two keys in .env (names are read by the published config, not hardcoded by the package):

RECAPTCHA_SITE_KEY=6Lc...
RECAPTCHA_SECRET_KEY=6Lc...
RECAPTCHA_SKIP_IP=127.0.0.1
RECAPTCHA_CURL_TIMEOUT=10

Then pick the widget flavour in config/recaptcha.php under the version key. The accepted values differ between major versions of the package (v2, v3, invisible on 5.x; v3/enterprise on 6.x) — check the README for the tag you installed, because a mismatch between key type and version fails silently rather than loudly.

First use case: protect a contact form

<form method="POST" action="/contact">
    @csrf
    <input name="email" type="email">
    <textarea name="message"></textarea>

    {!! Recaptcha::render() !!}

    <button type="submit">Send</button>
</form>
$request->validate([
    'email'                 => 'required|email',
    'message'               => 'required',
    'g-recaptcha-response'  => 'required|recaptcha',
]);

That is the canonical path: render the widget, then let the registered recaptcha validation rule verify the token server-side. If that works, everything else in the package is a variation on it.

Implementation Patterns

Form Request encapsulation. Don't repeat the rule in every controller. Put it on the Form Request so the verification is declarative and testable:

class ContactRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email'                => 'required|email',
            'message'              => 'required',
            'g-recaptcha-response' => ['required', 'recaptcha'],
        ];
    }
}

For v3, where the widget is a script rather than a checkbox, drop required (the field can legitimately be empty if the JS didn't load) and rely on the rule alone, or fail closed deliberately depending on your threat model.

Manual verification when the rule doesn't fit. The rule reads g-recaptcha-response from the request, which is fine until you need to verify a token arriving under a different name (API endpoint, AJAX payload, mobile client). Call the facade directly:

use Biscolab\ReCaptcha\Facades\ReCaptcha;

if (! Recaptcha::validate($request->input('captcha_token'))) {
    abort(422, 'Captcha verification failed');
}

Wrap this in your own middleware when you want to protect a route group rather than individual actions — the package does not ship a middleware, so this is a two-file job (middleware + Kernel/route registration):

public function handle($request, Closure $next)
{
    if (! Recaptcha::validate($request->input('g-recaptcha-response'))) {
        return response()->json(['message' => 'Captcha failed'], 422);
    }
    return $next($request);
}

Be aware that if you go this route, the token is consumed once; do not let it run and run the validation rule on the same request.

v3 with actions and score. For v3, render with an action name so Google can distinguish traffic and so you can assert the action server-side:

{!! Recaptcha::render('contact_form') !!}

The package's own rule is a success/failure check; it does not enforce a minimum score. If score matters to you, do it yourself after verification — the raw response exposes score and action, and you should reject anything under your threshold (0.5 is a common starting point) and anything whose action doesn't match the form you rendered. This is the single most-missed step when teams "enable v3" and instantly get spam again.

Invisible variant. Point the widget at the submit button rather than leaving it standalone; the token is injected into the form on submit. The server-side story is identical to v2 — same rule, same field name.

Local development. Use RECAPTCHA_SKIP_IP (comma-separated) so the test suite's requests and your browser don't have to solve a challenge on every submit. 127.0.0.1 and ::1 both need listing. This is the fastest way to keep Dusk/feature tests green without touching the container.

Gotchas and Tips

The package is archived. This is the headline risk. It received no updates since March 2024 and will receive none. That means no patches for a Google API change and no security fixes in the dependency chain. For a greenfield project, prefer anhskohbo/no-captcha, Google's official google/recaptcha PHP library wrapped in a small service, or an actively maintained fork. If you inherit this package, pin the exact version in composer.json, and isolate the calls behind a small interface so swapping it later is a one-file change rather than a grep across the codebase.

It uses cURL, not the Laravel HTTP client. The curl_timeout config key is the giveaway. Practical consequence: Http::fake() will not intercept the verification call, so the usual Laravel testing idiom does nothing. To test, either skip IPs in the test environment, or mock the ReCaptcha facade/binding. Don't discover this by wondering why your CI is hitting Google.

Version mismatch is a silent failure. A v2 site key with version => 'v3' (or vice versa) produces a widget that renders but never validates, or a validation that always fails. Google's error codes are surfaced through the package's response, but they are easy to swallow if your validation message is generic. Log the raw response at debug level the first time you wire a form up.

The field name is hardcoded. The rule and the renderer both assume g-recaptcha-response. If you rename the input for styling reasons, the rule will read an empty value and fail. Use the manual validate() call instead of fighting the convention.

Tokens are single-use. Re-validating a token — because you added a second middleware, or because a retried request reuses the payload — will fail with a timeout/duplicate error, not a clean rejection. Validate exactly once per request.

skip_ip and proxies. Skipping is based on request()->ip(). Behind a load balancer or Cloudflare without correct TrustProxies configuration, every request looks like it comes from the proxy address, which either skips everything (if you listed the proxy) or skips nothing. Verify what $request->ip() actually returns in your production environment before trusting the skip list.

Config caching. env() is read inside the published config, so after changing keys you need php artisan config:clear in local development. A stale cached config with the old secret key is a classic "it worked yesterday" failure.

Blade escaping. The render call emits HTML, so it must be {!! Recaptcha::render() !!}, not {{ }}. Using the escaped form produces a page full of visible &lt;script&gt; and a widget that never loads.

Extension points are thin. The tag_attributes config lets you attach CSS classes/attributes to the widget container, and the facade can be swapped in the container for testing. Beyond that, the package is a thin wrapper — if you need enterprise features (project IDs, assessment-based scoring, private network endpoints), you will be writing that integration yourself, and with the package archived, you should plan to hand it to Google's SDK directly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core