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+.
Pros:
with*() builders (withExpectedHostname(), withExpectedApkPackageName(), withExpectedAction(), withScoreThreshold(), withChallengeTimeout()) return clones instead of mutating state (#637). This makes ReCaptcha safe to bind as a singleton in Laravel's container under Octane, FrankenPHP, RoadRunner, and Swoole, where the previous mutating setters risked cross-request configuration bleed.int $timeout = 60 on CurlPost, Post, and SocketPost, plus CURLOPT_CONNECTTIMEOUT (#636). The prior assessment flagged timeouts as a fixed latency risk; they are now tunable per environment via config/services.php.Response implements \JsonSerializable (#636). The previous readonly/DTO serialization risk is largely eliminated—responses can be json_encode()d directly for caching, queue payloads, and API responses without custom shims.200 validation across CurlPost, Post, and SocketPost with consistent ReCaptcha::E_BAD_RESPONSE (#637). Deterministic error mapping in middleware instead of transport-specific surprises.CurlPost lazily initializes and reuses its CurlHandle for TLS session resumption and HTTP keep-alive (#637)—directly reduces p95 on high-volume verification paths. SocketPost now loops fwrite() for partial TLS writes and closes handles cleanly on failure.#[\SensitiveParameter] on $secret in ReCaptcha::__construct() and RequestParameters::__construct() (#636) prevents the secret surfacing in Laravel's exception handler, Ignition, Sentry, and log stack traces.ReCaptcha::SITE_VERIFY_URL_ALTERNATIVE (www.recaptcha.net) (#636) enables deployments where www.google.com egress is blocked—previously a hard blocker for some regional/CI environments.setScoreThreshold(0.0) no longer fails open when score is null (#637), and setChallengeTimeout() now fails closed with E_CHALLENGE_TIMEOUT on empty/unparseable challenge_ts (#637). Both were fail-open auth weaknesses in request-path verification.error-codes filtered to string elements in Response::fromJson() (#636), preventing type errors in strict-typed consumers and sanitizers.Cons:
challenge_ts handling now fails closed. Any integration that omits challenge_ts, synthesizes tokens in tests, or runs on hosts with clock skew may now receive E_CHALLENGE_TIMEOUT where it previously passed. This is a security-correct change but an observable behavioral one.CurlHandle instances persist across requests in Octane workers. Stale keep-alive connections, DNS/IP changes, or proxy re-resolution can surface as intermittent failures requiring retry logic.60s default is inappropriate for a synchronous form-submission path; it must be explicitly lowered.>=8.4 Hard Floor: The 2.x line requires PHP 8.4, which constrains the Laravel matrix to 10+/11+/12+.$this->app->singleton(\ReCaptcha\ReCaptcha::class, function ($app) {
return (new \ReCaptcha\ReCaptcha(
secret: config('services.recaptcha.secret'),
transport: new \ReCaptcha\RequestMethod\CurlPost(
timeout: (int) config('services.recaptcha.timeout', 5),
),
))
->withExpectedHostname(config('services.recaptcha.hostname'))
->withExpectedAction(config('services.recaptcha.action'))
->withScoreThreshold((float) config('services.recaptcha.score_threshold', 0.5))
->withChallengeTimeout((int) config('services.recaptcha.challenge_timeout', 120));
});
Note: with*() returns a clone—do not discard the return value, and do not call legacy setters on the resolved singleton.services.recaptcha.verify_url defaulting to ReCaptcha::SITE_VERIFY_URL, overridable to SITE_VERIFY_URL_ALTERNATIVE for restricted-egress environments.!isSuccess() as a generic bot:
$response = $recaptcha->verify($request->input('g-recaptcha-response'), $request->ip());
return match (true) {
$response->isSuccess() => $next($request),
in_array(ReCaptcha::E_CHALLENGE_TIMEOUT, $response->getErrorCodes(), true) => back()->withErrors(['recaptcha' => 'Verification expired, please retry']),
in_array(ReCaptcha::E_BAD_RESPONSE, $response->getErrorCodes(), true) => back()->withErrors(['recaptcha' => 'Verification unavailable, please retry']),
default => back()->withErrors(['recaptcha' => 'Verification failed']),
};
Mockery doubles on the transport remain valid (transport constructors only gained an optional parameter). New test cases worth adding: empty challenge_ts, score omitted with threshold 0.0, non-200 transport responses, non-string error-codes, and a json_encode($response) contract test.composer update google/recaptcha; 2.1.0 is verified backward-compatible with 2.0.0 via roave/backward-compatibility-check. Effort concentrates on behavioral validation (fail-closed paths), not API rewrites.High:
JsonSerializable added; 2.1.0 is BC with 2.0.0).Medium:
challenge_ts Semantics: Silent auth-bypass removal can produce new user-facing failures (expired v3 tokens, clock drift, load-balanced hosts without NTP). Must be validated against real traffic and paired with a distinct user-facing retry message.setExpectedHostname() / setScoreThreshold() on the injected singleton reintroduces cross-request mutation. Static analysis / code review rule required.Low:
jsonSerialize() Output as Public Contract: Once response JSON is persisted or emitted, its shape becomes an API surface—treat changes to it as versioned.error-codes String Filtering: Consumers assuming non-string entries will silently see fewer codes.#[\SensitiveParameter] Scope: Prevents future leakage only; secrets already captured in historical logs/APM traces are unaffected and may warrant rotation.challenge_ts (custom token wrappers, v3 invisible flows, E2E test harnesses)? Is NTP/clock sync guaranteed across all app hosts and load balancers?timeout/connect_timeout values should be standardized per environment? Should non-critical forms fail open to a challenge, or hard-fail?SITE_VERIFY_URL_ALTERNATIVE, and is it a static config or a runtime fallback?error-codes values or assume non-string entries?ReCaptcha as a singleton using the immutable builder chain (see snippet above). Bind RequestMethod\CurlPost explicitly so the timeout is ours, not the 60s default; keep Post/SocketPost only as fallbacks for environments without cURL.config/services.php → recaptcha):
secret, site_key, versiontimeout (default 5), connect_timeout (default 2)verify_url (default ReCaptcha::SITE_VERIFY_URL, override to SITE_VERIFY_URL_ALTERNATIVE)hostname, apk_package_name, action, score_threshold (default 0.5), challenge_timeout (default 120)E_CHALLENGE_TIMEOUT, E_SCORE_THRESHOLD_NOT_MET, E_BAD_RESPONSE).getErrorCodes() and request context only—never the token or secret. #[\SensitiveParameter] protects stack traces; it does not protect hand-written logger() calls, so redact explicitly.e().with*() on a fresh local variable, not on the injected instance.setExpectedHostname, setExpectedAction, setScoreThreshold, setChallengeTimeout, SocketPost, Post, and any direct Response serialization.>=8.4 and Laravel >=10 in composer.json; identify any service still pinned to 1.x.challenge_ts-bearing tokens (i.e., server-side verification of freshly issued tokens).composer update google/recaptcha on a non-critical module; wire CurlPost with a 2s connect / 5s total timeout.challenge_ts, score: null with threshold 0.0, non-200 responses, and json_encode($response).with*() across all call sites; remove remaining mutating setters.E_BAD_RESPONSE on non-user-blocking paths only.1.x line (no readonly, no #[SensitiveParameter], no 8.4 requirement).2.1.0 is 100% backward-compatible with 2.0.0 per roave/backward-compatibility-check; constructor additions are optional parameters, so existing transport instantiation compiles unchanged.challenge_ts, strict score null handling, and error-codes string filtering alter outcomes for edge inputs. These are the real upgrade risks, not signatures.curl and openssl required for connection reuse and TLS hardening; environments falling back to SocketPost still benefit from unified status validation but not from keep-alive.success/error-codes presence and types.curl/openssl enabled; config/services.php extended with timeout/endpoint/threshold keys; NTP verified.composer update google/recaptcha; replace singleton construction with the immutable builder chain; remove mutating setters.with*() returns and null-safety regressions); run the new edge-case tests; replay recorded production traffic against staging to detect any newly-failing challenge_ts paths.E_CHALLENGE_TIMEOUT and E_BAD_RESPONSE rate deltas; confirm no secret appears in logs post-deploy.2.1.0 is a minor release with a 2.x BC promise and automated BC verification upstream. Pin to ^2.1 and let Renovate/Dependabot surface patches; the maintenance burden shifts from code churn to config tuning (timeouts, thresholds) and periodic secret rotation.php-cs-fixer custom rule rather than documentation alone.E_CHALLENGE_TIMEOUT (clock skew / stale token → ask user to retry), E_SCORE_THRESHOLD_NOT_MET (expected bot signal — do not page), E_BAD_RESPONSE (transport/egress problem — check proxy, DNS, alternative endpoint), and E_SCORE_THRESHOLD_NOT_MET with score: null (API contract anomaly).#[\SensitiveParameter], but support scripts that dump request payloads must still be reviewed.CurlPost combination.SITE_VERIFY_URL_ALTERNATIVE removes a hard scaling wall for restricted-egress regions; make it a config toggle so a single image serves all regions.E_BAD_RESPONSE under Octane. Mitigation: retry once on a fresh transport instance; monitor error-code rate by worker age.challenge_ts → elevated E_CHALLENGE_TIMEOUT for clients with clock skew or slow token submission. Mitigation: generous but bounded withChallengeTimeout() (e.g., 120s), explicit "please retry" UX, monitor submission latency.#[\SensitiveParameter] fix. Mitigation: secret rotation, log scrubbing, verify no logger() call passes the secret.jsonSerialize() output consumed by caches/queues/SPAs; changing it later is a breaking change. Mitigation: version or normalize before persisting.with*() is a familiar Laravel pattern (immutable config objects); the main conceptual shift is "never mutate the injected instance."with*() return values used, (2) no legacy setters on singletons, (3) timeouts explicitly configured, (4) all error codes mapped distinctly, (5) secrets redacted in hand-written logging, (6) escaping verified on any reCAPTCHA-derived output.config/services.php comments, the team runbook (error-code table), and the onboarding guide's "verifying reCAPTCHA" snippet—these three artifacts account for most of the ongoing support load.How can I help you explore Laravel packages today?