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

google/recaptcha

google/recaptcha is the official PHP client for server-side verification of reCAPTCHA v2 and v3 responses, helping protect forms and APIs from spam and abuse. It supports Composer installation and requires PHP 8+.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Requirements & Installation — google/recaptcha:^2.1 requires PHP >= 8.4. That's the biggest change from the 1.5 line: this is a real upgrade blocker for apps on PHP 8.2/8.3. Plan the language upgrade before pulling the package.

    composer require google/recaptcha:^2.1
    

    No package service provider is shipped — this is a framework-agnostic library. You wire it into Laravel yourself.

  2. Configure Keys (config/services.php + .env)

    RECAPTCHA_SITE_KEY=your_site_key
    RECAPTCHA_SECRET=your_secret_key
    RECAPTCHA_SCORE_THRESHOLD=0.5
    RECAPTCHA_ACTION=submit
    
  3. Bind a Configured Instance in AppServiceProvider::register():

    use ReCaptcha\ReCaptcha;
    use ReCaptcha\RequestMethod\CurlPost;
    
    $this->app->singleton(ReCaptcha::class, function () {
        return (new ReCaptcha(config('services.recaptcha.secret'), new CurlPost(10)))
            ->withScoreThreshold((float) config('services.recaptcha.score_threshold', 0.5))
            ->withExpectedAction(config('services.recaptcha.action', 'submit'));
    });
    

    The new immutable with*() methods make this safe as a singleton — including under Laravel Octane, RoadRunner, and long-lived queue workers.

  4. First Verification

    use ReCaptcha\ReCaptcha;
    use ReCaptcha\Response;
    
    /** @var Response $response */
    $response = app(ReCaptcha::class)->verify(
        $request->input('g-recaptcha-response'),
        $request->ip()
    );
    
    if (! $response->isSuccess()) {
        // $response->getErrorCodes() returns ReCaptcha::E_* constants
    }
    

Implementation Patterns

1. Frontend Widget (v3)

<script src="https://www.google.com/recaptcha/api.js?render={{ config('services.recaptcha.site_key') }}"></script>
<script>
  grecaptcha.ready(() => {
    grecaptcha.execute('{{ config('services.recaptcha.site_key') }}', { action: 'submit' })
      .then(token => document.querySelector('#g-recaptcha-response').value = token);
  });
</script>

2. Custom Validation Rule (Laravel-idiomatic)

The package has no built-in validation rule, so wrap verification in one:

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use ReCaptcha\ReCaptcha;

class Recaptcha implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $response = app(ReCaptcha::class)->verify((string) $value, request()->ip());

        if (! $response->isSuccess()) {
            $fail('Captcha verification failed: '.implode(', ', $response->getErrorCodes()));
        }
    }
}

Then in a FormRequest:

public function rules(): array
{
    return [
        'g-recaptcha-response' => ['required', 'string', new \App\Rules\Recaptcha()],
    ];
}

3. Action / Hostname Pinning (recommended for v3)

Pin the expected action and hostname so tokens minted elsewhere are rejected:

$recaptcha = app(ReCaptcha::class)
    ->withExpectedAction('login')
    ->withExpectedHostname('app.example.com');

$response = $recaptcha->verify($token, $request->ip());

with*() returns a clone, so the shared singleton keeps its base configuration intact between requests.

4. JSON responses and API endpoints

ReCaptcha\Response now implements \JsonSerializable:

return response()->json([
    'ok'       => $response->isSuccess(),
    'score'    => $response->getScore(),
    'failures' => $response->getErrorCodes(),
]);
// or simply, since it is JsonSerializable:
return response()->json($response);

Useful for SPA/mobile responses and for structured request logging.

5. Custom Transport & Timeouts

All transports accept an int $timeout = 60 parameter and now share HTTP‑200‑only validation:

use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod\CurlPost;   // keep-alive + TLS session reuse
use ReCaptcha\RequestMethod\Post;       // stream-based
use ReCaptcha\RequestMethod\SocketPost; // raw socket fallback

$recaptcha = new ReCaptcha($secret, new CurlPost(timeout: 5));

Pass a short timeout for user-facing requests; use a larger one from background jobs.

6. Alternative Global Endpoint

For environments where www.google.com is blocked:

use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod\CurlPost;

$recaptcha = new ReCaptcha(
    $secret,
    new CurlPost(10, ReCaptcha::SITE_VERIFY_URL_ALTERNATIVE),
);
// resolves to: https://www.recaptcha.net/recaptcha/api/siteverify

7. Octane / Queue Workers

Because with*() clones, you can safely reuse one ReCaptcha instance in persistent workers:

$perRequest = app(ReCaptcha::class)->withExpectedAction($action);

Avoid the old set*() mutators on the singleton — they mutate shared instance state.

Gotchas and Tips

Breaking / Migration Notes (1.5 → 2.1.0)

  • PHP 8.4 minimum. If you can't upgrade PHP yet, stay on the 1.x line.
  • Namespace is ReCaptcha\, not Google\Recaptcha\. Classes: ReCaptcha\ReCaptcha, ReCaptcha\Response, ReCaptcha\RequestMethod\*.
  • No Laravel service provider or validation trait ships with the package. You provide the singleton binding and validation rule yourself (see above).

Immutability: set*() vs with*()

  • New immutable: withExpectedHostname, withExpectedApkPackageName, withExpectedAction, withScoreThreshold, withChallengeTimeout — each returns a new ReCaptcha.
  • Legacy set*() methods are still present but mutate in place. Keep them out of singletons, Octane, and long-lived workers.

Fail‑Closed Behaviour (new in 2.1.0)

  • Score threshold 0.0 no longer passes when Google omits score (null → ReCaptcha::E_SCORE_THRESHOLD_NOT_MET).
  • Challenge timeout no longer passes when challenge_ts is empty or unparseable (ReCaptcha::E_CHALLENGE_TIMEOUT).
  • error-codes is filtered so only string elements are retained — safe to implode()/log, but don't assume Google's raw array shape.

HTTP Status Validation

  • Non‑200 responses from any transport (CurlPost, Post, SocketPost) consistently surface as ReCaptcha::E_BAD_RESPONSE. Previously stream/socket transports could silently differ under proxies or error pages. Inspect getErrorCodes() rather than trying to read HTTP status directly.

Secret Leakage Protection

#[\SensitiveParameter] is now applied to $secret in ReCaptcha::__construct() and RequestParameters::__construct(). Stack traces redact it in 2.1.0 — older versions do not. Still avoid logging secrets in query strings regardless of version.

Connection Reuse

  • CurlPost now lazily creates and reuses its CurlHandle across submit() calls, so a singleton ReCaptcha gets HTTP keep-alive and TLS session resumption for free.
  • Consequence: don't share a CurlPost instance across PHP processes; per-worker singletons are fine. If you need per-call isolation, construct a fresh CurlPost.

Testing

  • Use Google's test keys in .env.testing:
    RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
    RECAPTCHA_SECRET=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
    
  • Bind a fake in HTTP tests:
    $this->mock(ReCaptcha::class, fn ($m) =>
        $m->shouldReceive('verify')->andReturn(new \ReCaptcha\Response(true))
    );
    
  • Response is still directly constructible (new Response(true)), convenient for fixtures.
  • Since Response is JsonSerializable, you can assert against json_encode($response) in feature tests.

Common Pitfalls

  1. IP mismatch — always pass $request->ip(). Behind proxies (Cloudflare, ALB) configure TrustProxies; otherwise Google scores against the proxy IP and you'll see mysterious failures.
  2. Single-use tokens — never cache g-recaptcha-response or the Response. Re-submitting yields timeout-or-duplicate. Don't stash the token in a queued job payload.
  3. Action mismatch on v3 — withExpectedAction() is case-sensitive and must match the frontend action exactly.
  4. Hostname matching is case-insensitive, but pinning via withExpectedHostname() is the only way to catch domain misconfiguration early.
  5. Timeout budget — the default 60 s will sit inside your request. Use new CurlPost(5) (or similar) on user-facing paths.

Extension Points

  • Custom transport: implement ReCaptcha\RequestMethod (submit(RequestParameters $params): string). Useful if you already tune a Guzzle/Symfony HTTP client, or for test doubles.
  • Alternative endpoint: new CurlPost($timeout, ReCaptcha::SITE_VERIFY_URL_ALTERNATIVE) for geographies where google.com is unreachable.
  • Custom Response: subclassing ReCaptcha\Response is possible — if you do, verify JsonSerializable output still matches what API consumers expect.
  • Per-environment keys: keep one singleton binding, swap config('services.recaptcha.secret') per environment.

Security Checklist (2.1.0)

  • PHP >= 8.4.
  • ReCaptcha registered as a singleton; use with*() for per-request tweaks, not set*().
  • Action and hostname pinned via withExpectedAction() / withExpectedHostname() for v3.
  • Transport timeout set to a sensible value (< 10 s on HTTP requests).
  • Failed attempts logged with getErrorCodes() + IP.
  • Secrets not logged; rely on #[\SensitiveParameter] only on 2.1.0+.
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.
skyraptor/chatter
edlugz/sasapay
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