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

mostafaznv/recaptcha

Laravel integration for Google reCAPTCHA v2 and v3, with simple validation rules, Blade rendering, and config-driven keys. Protect forms and APIs from spam and bots with minimal setup and no boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up in under five minutes:

composer require mostafaznv/recaptcha
php artisan vendor:publish --tag=laracaptcha-config

Add the keys to .env (the published config reads from these):

RECAPTCHA_SITE_KEY=your-site-key
RECAPTCHA_SECRET_KEY=your-secret-key
RECAPTCHA_ACTIVE=true

Register the keys for the domains you actually use in the Google reCAPTCHA admin console — v3 keys are domain-scoped, so a key created for example.com will silently return low scores or fail verification on staging.example.com and localhost.

Where to look first: the published config/laracaptcha.php is the source of truth for option names (site key, secret key, active flag, badge position, language). Read it before copying any code from a blog post — this is a small package and option names have shifted between releases. Then look at vendor/mostafaznv/recaptcha/src/ — it is small enough to read in one sitting, and you will need to know exactly how the rule posts to Google when you debug it.

First use case — a contact form. Render the widget inside the form and validate the returned token:

<form id="contact" method="POST" action="{{ route('contact.store') }}">
    @csrf
    <input name="email" type="email">
    <textarea name="message"></textarea>
    <button type="submit">Send</button>

    {{ LaraCaptcha::render('contact') }}
</form>
use Mostafaznv\LaraCaptcha\Rules\LaraCaptchaRule;

public function store(Request $request)
{
    $request->validate([
        'email'     => ['required', 'email'],
        'message'   => ['required'],
        'g-recaptcha-response' => ['required', new LaraCaptchaRule()],
    ]);

    // ...
}

If the form renders but nothing is validated, check the browser network tab for a POST to www.google.com/recaptcha/api/siteverify — if it is absent, the render call never bound to your form (usually an id mismatch).

Implementation Patterns

Render per-form, once per page. The render helper injects a hidden g-recaptcha-response field and the JS that populates it for the form whose id you pass. For pages with multiple forms (login + newsletter on one page), call it once per form id:

{{ LaraCaptcha::render('login') }}
{{ LaraCaptcha::render('newsletter') }}

Do not loop it inside a @foreach of forms without unique ids; the JS binding is id-based.

Validate in a FormRequest, not the controller. The rule is a standard Laravel rule object, so it composes normally:

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

Handle AJAX / SPA submissions explicitly. Server-rendered forms are the happy path; for fetch/Axios you must execute the widget yourself and send the token:

const token = await grecaptcha.execute('{{ config('laracaptcha.site_key') }}', { action: 'contact' });
await axios.post('/contact', { ...data, 'g-recaptcha-response': token });

Keep the action string stable per form and (if you add your own verification) assert it server-side — otherwise a token minted for your newsletter form can be replayed against your login form.

Bypass in tests and local dev. Set RECAPTCHA_ACTIVE=false in .env.testing and phpunit.xml, or the rule will make a real HTTP call to Google on every feature test and fail in CI. If you want to assert that verification happens, use Http::fake() and assert the siteverify request was sent rather than hitting the network:

Http::fake([
    'www.google.com/recaptcha/api/siteverify' => Http::response(['success' => true, 'score' => 0.9]),
]);

Pair with a score check for sensitive endpoints. The bundled rule answers "is this token valid?". For password resets and registration you usually also want a minimum score. Inspect the verification response yourself (or wrap the rule) and reject anything below your threshold, e.g. 0.5.

Gotchas and Tips

  • v3 is invisible and score-based — there is no challenge to fail. Users will never see a checkbox; a blocked submission just looks like a validation error. Log the returned score and action so support can diagnose "the form won't submit" tickets. Rejecting on success === false alone will not stop scripted abuse.
  • Tokens are single-use and expire in ~2 minutes. If your form takes a while to fill, or you re-execute on retry without a fresh token, verification fails. Re-run grecaptcha.execute on every submit, including retries.
  • The form id must match exactly. LaraCaptcha::render('contact') bound to <form id="contact-us"> renders and returns a token-less submission. Check the hidden input's value just before submit in DevTools — empty means the binding never fired.
  • Config caching surprises. After changing .env keys run php artisan config:clear. A stale config:cache pointing at your dev site key is the most common "works locally, fails in staging" cause.
  • Domain restrictions are silent. If the key is not authorized for the current hostname, Google returns an empty token and the server-side check fails. Verify the exact hostname (including www. and subdomains) in the reCAPTCHA console.
  • Debug by reading the verification request. When a submission fails, dump the response from siteverify — it includes error-codes (invalid-input-secret, timeout-or-duplicate, invalid-input-response), which tell you immediately whether it is a key problem, a replayed token, or a bad token.
  • The package is small and last released in early 2021. Read the rule and facade source before relying on them; pin the version in composer.json and be prepared to swap the validation rule for ~15 lines of your own Http::post to siteverify if you hit a Laravel/PHP incompatibility. The render helper is thin sugar over Google's documented JS snippet anyway.
  • Extension points: the config file (toggle on/off per environment, badge position/visibility), the rule class (subclass it to add score and action enforcement), and Laravel's HTTP client fake for tests. There is not much else to hook into — treat the package as a convenience wrapper, not a framework.
  • Accessibility/UX note: with the badge hidden, users get no indication that bot protection is active. If you hide it, add a short note near your submit button for transparency.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package