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

Recaptcha Bundle Laravel Package

mael/recaptcha-bundle

Mael RecaptchaBundle integrates Google reCAPTCHA into your PHP application's forms to help block spam and automated bot submissions. It provides straightforward configuration and server-side verification of user responses, making it easy to add basic bot protection to existing forms.

View on GitHub
Deep Wiki
Context7

Getting Started

Stack reality check first. This is a Symfony bundle (MaelRecaptchaBundle), not a Laravel package. There is no service provider, no config/recaptcha.php, no Blade directives, and no ValidationRule. A Laravel developer has exactly two viable ways to "leverage" it:

  1. As a reference implementation — read the source to understand the correct flow, then write the ~40–100 lines of Laravel-native glue yourself.
  2. In a Symfony app you also maintain — install it directly, but pin and own it, given the 2020 release.

For day-to-day Laravel work, treat (1) as the primary path. The correct adoption target in a Laravel codebase is biscolab/laravel-recaptcha or anhskohbo/no-captcha, wrapped in your own form request + validation rule so the vendor can be swapped.

Reading order (what to look at first):

  1. README.md — the exact Twig function name, form type name, and config keys. Small single-maintainer bundles rename these between releases; verify against the tagged version you're reading, don't assume.
  2. DependencyInjection/Configuration.php — the authoritative list of options and defaults (this is where site_key / secret_key and any timeout/verify-URL options actually live).
  3. Form/Type/ — how the widget is added to a Symfony form, and what the submit path looks like.
  4. Validator/Constraints/ — how the g-recaptcha-response token is checked server-side. This is the code you're going to copy.
  5. Resources/views/ — the Twig template that injects https://www.google.com/recaptcha/api.js.

First use case: guard a contact form. In Symfony that means adding the bundle's recaptcha form type to ContactType; in Laravel it means a Blade component that renders the widget and a Recaptcha validation rule that calls siteverify on submit.

Smoke-testing without a real key pair: Google publishes always-passing test keys — 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI (site) and 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe (secret). Use them to confirm your wire-up end-to-end before provisioning production keys, and only in local/test environments.

Implementation Patterns

Pattern 1 — Port the verifier, don't port the bundle

The bundle does one essential thing: POST secret, response, and remoteip to https://www.google.com/recaptcha/api/siteverify and inspect the JSON. In Laravel this becomes a small service behind an interface, which is the single most valuable thing you get from reading this package: the shape.

// app/Services/Recaptcha/RecaptchaVerifier.php
interface RecaptchaVerifier
{
    public function verify(string $token, ?string $ip = null, ?string $action = null): RecaptchaResult;
}

final class GoogleRecaptchaVerifier implements RecaptchaVerifier
{
    public function __construct(
        private readonly string $secret,
        private readonly float $minScore = 0.5,
        private readonly int $timeout = 3,
    ) {}

    public function verify(string $token, ?string $ip = null, ?string $action = null): RecaptchaResult
    {
        $response = Http::asForm()->timeout($this->timeout)->post(
            'https://www.google.com/recaptcha/api/siteverify',
            array_filter([
                'secret'   => $this->secret,
                'response' => $token,
                'remoteip' => $ip,
            ]),
        );

        if ($response->failed()) {
            return RecaptchaResult::unavailable();
        }

        $body = $response->json();

        if (!($body['success'] ?? false)) {
            return RecaptchaResult::failed($body['error-codes'] ?? []);
        }

        if ($action !== null && ($body['action'] ?? '') !== $action) {
            return RecaptchaResult::failed(['action-mismatch']);
        }

        if (isset($body['score']) && $body['score'] < $this->minScore) {
            return RecaptchaResult::failed(['low-score']);
        }

        return RecaptchaResult::passed();
    }
}

Keep the interface so reCAPTCHA can later be swapped for Cloudflare Turnstile or hCaptcha without touching call sites.

Pattern 2 — Wrap it in a Laravel validation rule

This replaces the Symfony FormType + Validator\Constraints pairing the bundle ships. One rule, reused across every form request:

final class RecaptchaRule implements ValidationRule
{
    public function __construct(private readonly ?string $action = null) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (!is_string($value) || $value === '') {
            $fail('Please complete the CAPTCHA.');
            return;
        }

        $result = app(RecaptchaVerifier::class)->verify(
            $value,
            request()->ip(),
            $this->action,
        );

        if ($result->isUnavailable()) {
            // fail-closed by default; see Gotchas for the fail-open decision
            $fail('CAPTCHA verification is temporarily unavailable. Please retry.');
            return;
        }

        if (!$result->passed()) {
            $fail('CAPTCHA verification failed. Please retry.');
        }
    }
}

Then in a form request:

public function rules(): array
{
    return [
        'email'        => ['required', 'email'],
        'message'      => ['required', 'string', 'max:2000'],
        'g-recaptcha-response' => ['required', new RecaptchaRule('contact')],
    ];
}

Pattern 3 — Blade component instead of the Twig extension

The bundle exposes a Twig function that renders the api.js script and the widget. In Laravel this is a Blade component:

{{-- resources/views/components/recaptcha.blade.php --}}
@props(['action' => 'default'])

@once
    <script src="https://www.google.com/recaptcha/api.js" async defer></script>
@endonce

<div class="g-recaptcha"
     data-sitekey="{{ config('services.recaptcha.site_key') }}"
     data-callback="onRecaptcha_{{ str_replace('-', '_', $action) }}">
</div>

In a form:

<form method="POST" action="/contact">
    @csrf
    ...
    <x-recaptcha action="contact" />
    <button type="submit">Send</button>
</form>

Note the field name g-recaptcha-response is fixed by Google's script — it must match the rule's attribute key exactly.

Pattern 4 — Config placement (config/services.php, secrets in .env only)

The Symfony bundle puts keys in YAML; in Laravel, mirror the same separation of concerns:

// config/services.php
'recaptcha' => [
    'site_key'   => env('RECAPTCHA_SITE_KEY'),
    'secret_key' => env('RECAPTCHA_SECRET_KEY'),
    'min_score'  => env('RECAPTCHA_MIN_SCORE', 0.5),
    'timeout'    => env('RECAPTCHA_TIMEOUT', 3),
],

Never commit real keys. The bundle's YAML pattern is the exact anti-pattern that leads to secrets in git history if copied verbatim — use .env, .env.testing, and CI secrets.

Pattern 5 — Per-surface actions and thresholds

If you go with v3, pass a distinct action per surface (contact, signup, password_reset) and set thresholds per surface at the call site (new RecaptchaRule('signup') with a stricter minScore). This is the day-to-day pattern for scoping bot mitigation — it lets you escalate enforcement on registration without making every contact form unusable.

Gotchas and Tips

It's a Symfony bundle — do not composer require it into a Laravel app. It will install (PHP packages don't care), but nothing will auto-register, config/bundles.php won't exist, and you'll spend an afternoon debugging why nothing happens. The install path only applies to Symfony.

Do not treat this as a maintained dependency. Last release 2020-05-23, 2 stars, 0 dependents, single-maintainer namespace. Any release you take is a fork you own. For a Laravel app this is a non-issue if you follow Pattern 1; for a Symfony app it means pinning and budgeting for the eventual Guzzle/Twig/PHP-version conflicts.

success: true alone is not sufficient verification. The classic bypass is checking only the success flag. Always also check — at minimum — that the response corresponds to the expected action (v3), and, for sensitive surfaces, inspect challenge_ts freshness and hostname against your allow-list. The bundle's age makes it unlikely it validates anything beyond success; verify in Validator/Constraints/ before you trust it, and build the extra checks into your own port regardless.

Missing or empty token must fail, not pass. Ad blockers, restricted regions, and script-blocked browsers routinely omit g-recaptcha-response. Several naive verifier ports treat "no token" as "no CAPTCHA configured → allow." Audit for this. The rule above treats empty as a hard failure with a user-facing retry.

Malformed JSON from siteverify must not coerce to pass. If you see code like if (!isset($json['success']))) { return true; } in a port, that's a bypass. Require an explicit truthy success and an explicit parsed body; treat parse failure as unavailable.

Fail-open vs fail-closed is a per-surface product decision, not a default. Registration and password reset: fail closed. Low-risk contact/newsletter: fail open is defensible for conversion, but log it so you can see Google outages. Bake this into RecaptchaResult::unavailable() handling at the call site; don't hardcode it in the verifier.

siteverify is a synchronous third-party call on every protected submit. Tokens are single-use with ~2-minute validity, so you cannot cache or batch. Budget for per-request latency (typically 50–200 ms), a hard timeout (2–3 s), and connection reuse. This latency is the real scaling cost of any reCAPTCHA integration — not the package choice.

Test keys are always-pass and are the #1 source of prod incidents. Google's documented test key pair (6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI / 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe) always succeeds. Guard against leaking them into production config/services.php. A useful assertion: in production, throw on boot if RECAPTCHA_SECRET_KEY equals the test secret.

TLS verification is non-negotiable. If you find a HTTP::withoutVerifying() or 'verify' => false in an inherited HTTP client wired to siteverify, that's a bypass and a credential-handling risk. Keep Laravel's default Http (cURL with verification) and don't disable it during debugging.

v2 vs v3 is not a detail — it changes your whole UX contract. The bundle is 2020-era and almost certainly v2 checkbox only. If you're writing Laravel glue, decide v2 (explicit checkbox, user-visible friction) vs v3 (invisible, score-based, needs render=/onload= params and an action/threshold policy) up front. v3 requires grecaptcha.ready() + execute() in the Blade component; a static <div class="g-recaptcha"> is v2 and will silently do nothing on a v3 key or vice versa.

Secrets must not live in committed config. The Symfony YAML pattern (mael_recaptcha: { secret_key: ... }) is what gets copied into git. Use env() only, and keep .env.example with empty placeholders.

Extension points you actually want to keep. The bundle's shape — a service you can call and a form type — is the right shape to reproduce. Keep RecaptchaVerifier behind an interface, keep the rule small, keep the Blade component presentational. Then swapping Google for Turnstile/hCaptcha later (a privacy/GDPR question worth an explicit decision, not a default) is a single binding change in a service provider.

For Symfony users who do install it: check the bundle's Resources/config/services.yaml (or equivalent) for how the HTTP client is wired, and confirm the outbound client is not constructing its own cURL handle with verification disabled. Also check the Configuration.php for a verify-URL option — if it's hardcoded to https://www.google.com/recaptcha/api/siteverify, that's fine, but there's no Enterprise host override, which is one more reason to treat it as v2-only.

Logging and observability. Add a structured log line on every verifier outcome — passed, failed (with error-codes), unavailable — tagged by action and endpoint. Without this you can't tell a real bot spike from a Google outage from your own secret-rotation mistake, and the bundle gives you none of this out of the box.

Bottom line for day-to-day use: read it, port the ~40-line verify path, wrap it behind an interface and a ValidationRule, and use biscolab/laravel-recaptcha or anhskohbo/no-captcha as the reference Laravel integration rather than this Symfony bundle. The cost either way is 1–2 days; the decision that actually matters is which bot-mitigation vendor you standardize on and what your fail-open/fail-closed policy is per surface.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums