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

Recaptchaproxyable Laravel Package

jakreiter/recaptchaproxyable

A Laravel package that makes reCAPTCHA verification proxyable, letting you route validation requests through a proxy or custom transport instead of hitting Google directly. Useful for apps in restricted networks or regions where direct reCAPTCHA calls are unreliable, while keeping the familiar valid...

View on GitHub
Deep Wiki
Context7

Getting Started

This is a fork of Google's google/recaptcha PHP client with the addition of proxy support for the server-to-Google siteverify call. It is not a Laravel package — it ships no service provider, config, facade, or validation rule. You will be wiring it yourself. Treat it as a thin HTTP wrapper around the verification endpoint.

Install

composer require jakreiter/recaptchaproxyable

If google/recaptcha is also in your composer.json (directly or transitively), you will have two classes in the ReCaptcha\ namespace and Composer will pick whichever autoloads first. Remove the original before installing this one.

Where to look first

  1. src/ReCaptcha/ReCaptcha.php — the verify() entry point and the fluent setters (setExpectedHostname, setExpectedAction, setScoreThreshold, setChallengeTimeout). This mirrors the upstream API exactly.
  2. src/ReCaptcha/RequestMethod/CurlPost.php — this is the interesting file. Proxy options live here (or in a sibling Curl* class), so confirm how the fork exposes them: as constructor args, fluent setters, or a static config. The README is likely stale since the feature is "in progress" — read the source.
  3. src/ReCaptcha/Response.php — isSuccess(), getErrorCodes(), getScore(), getHostname(), getAction().

First use case

Server-side verification of a v2 checkbox token. The client-side widget is your responsibility:

<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form method="POST" action="/contact">
    @csrf
    <div class="g-recaptcha" data-sitekey="{{ config('services.recaptcha.site_key') }}"></div>
    <button>Send</button>
</form>

Then in the controller:

use ReCaptcha\ReCaptcha;

$recaptcha = new ReCaptcha(config('services.recaptcha.secret_key'));
$response  = $recaptcha->setExpectedHostname(request()->getHost())
                        ->verify(request('g-recaptcha-response'), request()->ip());

if (! $response->isSuccess()) {
    // $response->getErrorCodes() === ['invalid-input-response'] etc.
}

Get that working before touching the proxy. The proxy is an infrastructure concern, not a functional one — you don't want to debug two unknowns at once.

Implementation Patterns

Proxy configuration belongs in a custom request method

The cleanest integration is to treat the proxy as a request-method strategy, keeping it out of your controllers. The library's RequestMethod interface is small (submit(RequestParameters $params): Response), so if the fork's built-in proxy support doesn't fit your needs (e.g. you need SOCKS5, or a proxy per environment), implement it yourself and pass it to the constructor's second argument:

$recaptcha = new ReCaptcha($secret, new ProxyAwareCurlPost(
    proxy: config('services.recaptcha.proxy'),
    timeout: 5,
));

This is the extension point that makes the package worth forking rather than abandoning. Everything downstream (hostname checks, error codes, v3 scoring) stays upstream-compatible.

Service provider with a singleton factory

Register one factory, not one ReCaptcha instance — you'll want different keys or expected hostnames per form eventually.

// app/Providers/RecaptchaServiceProvider.php
public function register(): void
{
    $this->app->bind(ReCaptcha::class, function () {
        return new ReCaptcha(
            config('services.recaptcha.secret_key'),
            $this->app->make(\ReCaptcha\RequestMethod::class),
        );
    });
}

Bind RequestMethod conditionally on config('services.recaptcha.proxy') — null proxy returns a plain CurlPost, a string returns the proxy-aware variant. That single binding is where "we need a proxy in staging but not locally" lives.

Validation rule as the public surface

Controllers should never touch ReCaptcha directly. Wrap verification in a rule so it composes with Laravel's validation pipeline:

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)
            ->setExpectedHostname(request()->getHost())
            ->verify((string) $value, request()->ip());

        if (! $response->isSuccess()) {
            $fail('The reCAPTCHA check failed. Please try again.')
                ->translate(); // keep user-facing text vague on purpose
        }
    }
}

Log getErrorCodes() at warning level in the $fail branch — see Gotchas for why.

Handling v3 (score-based) alongside v2

This library supports both, but the semantics differ. For v3, isSuccess() means "the token was valid", not "the user is human". You must inspect getScore():

$response = $recaptcha
    ->setExpectedHostname(request()->getHost())
    ->setExpectedAction('contact_form')   // critical for v3
    ->setScoreThreshold(0.5)
    ->verify($token, request()->ip());

Two things to internalise:

  • Actions are per-form. Emit data-action="contact_form" on the JS side and assert setExpectedAction('contact_form') on the server. Without this, a token harvested from your newsletter form can be replayed against your password-reset form.
  • Don't use v3 for high-stakes actions alone. Score ≥ 0.5 is a heuristic, not a gate. Treat v3 as a risk signal and combine it with rate limiting for login, payments, and account deletion.

Testing with Http::fake won't work

This library uses raw curl, not Laravel's HTTP client — Http::fake() intercepts nothing here. Two options:

  1. Inject a fake RequestMethod. Easiest and fastest; bind a stub that returns canned Response objects. This is why the rule binds Recaptcha::class rather than new-ing it.
  2. Point the proxy at a local stub server. Use this when you actually need to test the proxy/curl path itself (e.g. verifying your proxy config handles a 502 without throwing).

Never let tests hit Google. The endpoint is rate-limited and your CI will flake.

Staged rollout of the proxy

If you're adding this specifically because Google is blocked from your network, do it in one PR: introduce the RequestMethod binding returning proxy-aware curl, deploy to staging, assert a real isSuccess() against a real token. Then flip production. Don't ship the proxy config and the feature integration in the same deploy — a misconfigured proxy will make every form on the site reject valid submissions, and you want to be able to attribute that failure.

Gotchas and Tips

"In progress" means the proxy API may change

The description explicitly says the proxy feature is in progress, the repo has zero stars and zero dependents, and the last release is recent. That's a fork with one active author, not a stable library. Pin the exact version in composer.json, and if the proxy semantics matter to you, vendor the one file you need (RequestMethod/CurlPost.php or its proxy variant) into app/Support/ rather than depending on a release cadence you don't control. The rest of the library is a decade-stable Google codebase — the fork risk is entirely in the new curl code.

Proxy environment variables are not read automatically

curl honours http_proxy / https_proxy / no_proxy for the libcurl default, but PHP's curl extension does not apply those unless you set CURLOPT_PROXY explicitly. If it "works on the box where I exported the var and not in the container", that's why. Check whether the fork reads env vars; assume it doesn't, and pass the proxy URI deliberately from config/services.php.

CURLOPT_SSL_VERIFYPEER will bite you

Corporate proxies that MITM TLS will fail verification. The temptation is CURLOPT_SSL_VERIFYPEER => false. Don't. Set CURLOPT_CAINFO to your proxy's CA bundle instead. A disabled peer check on the call that decides whether a user is a bot is a genuinely bad trade.

Timeouts must be short, and non-2xx must be caught

The default upstream timeout is generous (measured in seconds, and the curl method has historically been loose about it). Set CURLOPT_CONNECTTIMEOUT to ~2s and CURLOPT_TIMEOUT to ~5s. Your form must not hang on reCAPTCHA being unreachable — and when curl returns a transport error, isSuccess() is false with no error codes, indistinguishable from a bad token unless you inspect Response/log the exception. Fail closed on verification failure, but log the transport exception separately so a proxy outage doesn't look like a spam wave.

Error codes are your only debugging signal

verify() returns a Response whose getErrorCodes() is one or more of: missing-input-secret, invalid-input-secret, missing-input-response, invalid-input-response, bad-request, timeout-or-duplicate.

  • timeout-or-duplicate → your token is being replayed (double-submit from a form resubmit, or two validators on one request). Extremely common when both middleware and a validation rule verify the same token.
  • invalid-input-response → expired token (120s), already-used token, or wrong site key.
  • missing-input-secret/invalid-input-secret → your config is empty; happens when .env is cached from an old deploy.

Log the codes, never the token. Tokens are single-use but they still appear in logs verbatim from the client payload if you Log::info(request()->all()).

setExpectedHostname and ports

getHost() returns example.com:8080 behind a non-standard port; Google's hostname field does not include the port. Use request()->getHost() (no port), or parse_url(config('app.url'), PHP_URL_HOST). Behind a load balancer, confirm TrustProxies is configured or the hostname mismatch will reject everything in production while passing locally.

Remote IP is optional but use it

verify($token, $ip) — passing the IP lets Google correlate. Pass request()->ip(), and be aware that behind a proxy this is your load balancer's IP unless TrustProxies is set. An IP mismatch doesn't fail verification, but it degrades the risk score.

Don't verify the same token twice

The classic bug: a Recaptcha middleware that verifies on every POST plus a validation rule on the form. The second call returns timeout-or-duplicate and the user is rejected despite being human. Pick one integration point — I recommend the validation rule — and keep the middleware for global rate-limiting only.

Response getScore() is null for v2

If you write if ($response->getScore() < 0.5) you'll get null < 0.5 === true and reject every legitimate v2 submission. Guard on !== null, or key the check off the widget type explicitly in config.

Extension points worth knowing

RequestMethod (the interface) is the seam for proxy, logging, retries, and test doubles. Response is final-ish in upstream but the fork may expose it — if you need to attach metadata (proxy used, latency), decorate rather than subclass. And if the fork's proxy configuration is only reachable via a constructor arg on CurlPost, wrap it in your own RequestMethod that reads Laravel config instead of threading values through every instantiation.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route