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 Recaptchav3 Laravel Package

josiasmontag/laravel-recaptchav3

Laravel Recaptcha V3 is a lightweight package for backend validation of Google reCAPTCHA v3. It provides JS initialization, invisible form fields, and a recaptchav3 validator accepting an action and minimum score, configured via site and secret keys.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up credentials

composer require josiasmontag/laravel-recaptchav3
RECAPTCHAV3_SITEKEY=your-site-key
RECAPTCHAV3_SECRET=your-secret-key

The service provider and RecaptchaV3 facade are auto-discovered. Publish the config only if you need to change defaults:

php artisan vendor:publish --provider="Lunaweb\RecaptchaV3\Providers\RecaptchaV3ServiceProvider"

Where to look first (in this order):

  1. config/recaptchav3.php — sitekey, secret, and the HTTP options (timeout). This is 90% of the knobs you'll ever touch.
  2. Lunaweb\RecaptchaV3\RecaptchaV3 — getSiteKey() and verifyResponse($token, $action, $threshold).
  3. Lunaweb\RecaptchaV3\RecaptchaV3Rule — the validation rule; read it once to see exactly how score and action are compared.
  4. Lunaweb\RecaptchaV3\Response — the value object returned by verifyResponse() (isSuccess(), getScore(), getAction(), getErrorCodes()).

First use case: a contact form. Two moving parts — the browser gets a token, the server verifies it.

Frontend (fetch the token on submit, not on page load — tokens expire in ~2 minutes and are single-use):

<form method="POST" action="{{ route('contact.store') }}" id="contact-form">
    @csrf
    @recaptchaV3
    <input type="hidden" name="g-recaptcha-response" id="recaptcha-token">
    {{-- your fields --}}
</form>

<script>
document.getElementById('contact-form').addEventListener('submit', function (e) {
    if (document.getElementById('recaptcha-token').value) return;

    e.preventDefault();
    const form = e.target;

    grecaptcha.ready(function () {
        grecaptcha.execute('{{ config('recaptchav3.sitekey') }}', { action: 'contact_us' })
            .then(function (token) {
                document.getElementById('recaptcha-token').value = token;
                form.submit();
            });
    });
});
</script>

Backend:

use Lunaweb\RecaptchaV3\RecaptchaV3Rule;

$request->validate([
    'g-recaptcha-response' => ['required', new RecaptchaV3Rule('contact_us', 0.5)],
    'email' => ['required', 'email'],
]);

That's the whole happy path. Everything below is about doing it at scale.

Implementation Patterns

Rule-first for form posts, manual verification for everything else. The validation rule is the idiomatic path: it throws a ValidationException you already know how to surface, and it returns the error to the user in the standard $errors bag. Reach for the facade when there's no Request::validate() in play — API endpoints, Livewire actions, queued jobs that re-verify, or an Artisan command.

use Lunaweb\RecaptchaV3\Facades\RecaptchaV3;

$response = RecaptchaV3::verifyResponse(
    $request->input('g-recaptcha-response'),
    'newsletter_signup',
    0.5
);

if (! $response->isSuccess() || $response->getScore() < 0.5) {
    Log::warning('recaptcha rejected', [
        'action' => $response->getAction(),
        'score'  => $response->getScore(),
        'errors' => $response->getErrorCodes(),
        'ip'     => $request->ip(),
    ]);

    abort(422, 'Captcha verification failed.');
}

Check both isSuccess() and the score yourself in manual flows — it costs nothing and makes the failure mode explicit when you're debugging a threshold decision six months from now.

One action name per form, kept in a constant. Action names are free-form strings that Google echoes back. Typos are the #1 silent failure (action-mismatch in the error codes). Put them on a small class or in config so the Blade template and the rule can't drift:

final class RecaptchaAction
{
    public const CONTACT = 'contact_us';
    public const REGISTER = 'register';
    public const NEWSLETTER = 'newsletter_signup';
}

Log the score before you enforce it. When you first ship this, set the threshold low (or log-only) and watch the score distribution for a week. Real scores cluster at 0.7–0.9 for humans; a hard 0.9 cutoff will bounce legitimate users on corporate VPNs and privacy browsers. A useful pattern is a middleware that verifies and annotates the request, letting each controller decide whether a low score blocks or just flags the submission for moderation:

class VerifyRecaptcha
{
    public function handle($request, Closure $next, string $action, float $threshold = 0.5)
    {
        $result = RecaptchaV3::verifyResponse($request->input('g-recaptcha-response'), $action, $threshold);

        $request->attributes->set('recaptcha', $result);

        if (! $result->isSuccess() || $result->getScore() < $threshold) {
            abort(422, 'Captcha verification failed.');
        }

        return $next($request);
    }
}

Then Route::post('/contact', ...)->middleware('recaptcha:contact_us,0.5'). This keeps controllers clean and makes the policy visible in routes/web.php.

Testing. Don't let the suite make outbound HTTP calls to Google. Two viable approaches:

  • If your version of the package goes through Laravel's HTTP client: Http::fake(['https://www.google.com/recaptcha/api/siteverify' => Http::response(['success' => true, 'score' => 0.9, 'action' => 'contact_us'])]);
  • Otherwise, swap the container binding / facade with a fake in setUp() so verifyResponse() returns a stubbed Response.

Either way, write the failure test too (score 0.1, error code timeout-or-duplicate) — the passing path is never the one that breaks in production. Google's documented test keys are v2-oriented; don't assume they'll give you predictable v3 scores.

Multi-tenant / multiple domains. The client is a container singleton built from the config array at resolution time. If you need different keys per tenant, set the config before the client resolves, or forget/re-register the instance — mutating config() after the first verifyResponse() call silently does nothing.

Gotchas and Tips

  • Tokens are single-use and short-lived (~2 minutes). Never generate the token on page load for a form a user might sit on. Generate on submit. If your form does an XHR validation round-trip, generate a fresh token for the final submit.
  • The action must match. If the browser calls grecaptcha.execute(key, {action: 'contact_us'}) and the server verifies with 'login', verification fails regardless of score. Same string, both sides.
  • The score is a signal, not a verdict. It's a probability from a black box. Combine it with rate limiting, honeypots, and moderation queues rather than treating 0.4 as "definitely a bot". Always allow a fallback path for a real user who gets rejected.
  • g-recaptcha-response is not implicit. Pair the rule with 'required', otherwise a missing token sneaks past the rule's own checks (or produces a confusing "invalid response" message instead of "this field is required").
  • Config cache bites. RECAPTCHAV3_* changes require php artisan config:clear in dev. A stale cached config is the usual cause of "it works locally but not on the server".
  • The siteverify call is synchronous. Roughly 100–300 ms, sometimes worse. Set the config's HTTP timeout to something small (3–5 s) so a Google hiccup doesn't hang your request cycle. Don't call it twice for the same submission.
  • Error codes are your debugger. missing-input-response (token never reached the server — check the hidden field name), invalid-input-response (malformed/expired), timeout-or-duplicate (already used, or too old), action-mismatch, invalid-input-secret (wrong env). Log getErrorCodes() before you guess.
  • Never trust the score on the client side. The token means nothing until the server has verified it against Google; any client-provided "score" is trivially forgeable.
  • Message customisation. The rule's failure message is translatable — look up the exact key in src/RecaptchaV3Rule.php and override it in lang/{locale}/validation.php so you can write copy like "We couldn't verify your submission, please try again."
  • Don't gate login with a high threshold. Scoring failures on /login lock out real users with no recovery path. If you use it there, treat low scores as "require 2FA" rather than "deny".
  • Extension point worth using: wrap the RecaptchaV3 binding in your own decorator that logs getScore() / getAction() on every verification. You get a free dataset for tuning thresholds and can later feed it into an alert ("scores dropped for all users → check the key/project in the Google console").
  • Zero dependents and a small surface area means you should read the (short) source before assuming behaviour. The package deliberately does only backend verification — no Vue/React component, no invisible-widget markup, no automatic token injection. Frontend glue is yours, which is exactly why the "fetch on submit" pattern above is worth standardising in your codebase.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource