From b295c67cfc32dfc889dc92c365d98d2f4d32d73f Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 11 Jul 2023 21:44:45 +1200 Subject: [PATCH] Server-side validation for v3 recaptcha response fix #69 - server side validation of the recaptcha v3 verify response. - validator now returns true/false - validator checks action if specified - validator checks challenge_ts within 2 minutes - validator has configurable parameters for threshold of score and action - js object with execute method able to be generated which can facilitate promise based calling --- src/Facades/ReCaptcha.php | 2 ++ src/ReCaptchaBuilder.php | 29 ++++++++++++++- src/ReCaptchaBuilderV3.php | 60 ++++++++++++++++++++++++++++++++ src/ReCaptchaServiceProvider.php | 43 +++++++++++++++++++++-- src/helpers.php | 35 +++++++++++++++++++ 5 files changed, 165 insertions(+), 4 deletions(-) diff --git a/src/Facades/ReCaptcha.php b/src/Facades/ReCaptcha.php index ee49941..b1bc296 100644 --- a/src/Facades/ReCaptcha.php +++ b/src/Facades/ReCaptcha.php @@ -17,6 +17,8 @@ * @package Biscolab\ReCaptcha\Facades * * @method static string htmlScriptTagJsApi(?array $config = []) + * @method static string htmlScriptTagJsObjectV3() + * @method static string htmlScriptTagJsObjectV3WithDependency() * @method static string htmlFormButton(?string $button_label = '', ?array $properties = []) * @method static string htmlFormSnippet() * @method static string getFormId() diff --git a/src/ReCaptchaBuilder.php b/src/ReCaptchaBuilder.php index c97652c..db8552a 100755 --- a/src/ReCaptchaBuilder.php +++ b/src/ReCaptchaBuilder.php @@ -293,6 +293,32 @@ public function htmlScriptTagJsApi(?array $configuration = []): string return $html; } + /** + * Writes a HTML script tag that exposes a ReCaptchaV3 object for resolving the reCAPTCHA token. + * Insert this before the closing tag, following the htmlScriptTagJsApi call, as it does not load the reCAPTCHA script. + * + * The ReCaptchaV3 object in JavaScript has a method called execute that returns a promise resolving with a reCAPTCHA token. + * - action: string, defaults to 'homepage'. + * You may set this to a specific action, such as "contact_form_submit", based on the user's action. + * + * @return string The generated script HTML tag. + */ + public function htmlScriptTagJsObjectV3(): string + { + return ''; + } + + /*** + * The same as htmlScriptTagJsObjectV3 but it loads the reCAPTCHA script if the user is not skipped by IP. + * Can be used if you only want to include on specific pages but not send on page load. + * + * @return string + */ + public function htmlScriptTagJsObjectV3WithDependency(): string + { + return ''; + } + /** * Call out to reCAPTCHA and process the response * @@ -302,7 +328,7 @@ public function htmlScriptTagJsApi(?array $configuration = []): string */ public function validate($response) { - + // info("Recaptcha Validation called: ".$response); if ($this->skip_by_ip) { if ($this->returnArray()) { // Add 'skip_by_ip' field to response @@ -349,6 +375,7 @@ public function validate($response) return false; } $response = json_decode(trim($curl_response), true); + //info('Response from recaptcha'.json_encode($response)); if ($this->returnArray()) { return $response; diff --git a/src/ReCaptchaBuilderV3.php b/src/ReCaptchaBuilderV3.php index d57aef6..736bb57 100644 --- a/src/ReCaptchaBuilderV3.php +++ b/src/ReCaptchaBuilderV3.php @@ -63,6 +63,8 @@ public function getValidationUrlWithToken(): string * Write script HTML tag in you HTML code * Insert before tag * + * I suspect that this is used to inform reCAPTCHA about the page load. + * * @param array|null $configuration * * @return string @@ -118,4 +120,62 @@ public function htmlScriptTagJsApi(?array $configuration = []): string return $html; } + + /** + * Writes a HTML script tag that exposes a ReCaptchaV3 object for resolving the reCAPTCHA token. + * Insert this before the closing tag, following the htmlScriptTagJsApi call, as it does not load the reCAPTCHA script. + * + * The ReCaptchaV3 object in JavaScript has a method called execute that returns a promise resolving with a reCAPTCHA token. + * - action: string, defaults to 'homepage'. + * You may set this to a specific action, such as "contact_form_submit", based on the user's action. + * + * @return string The generated script HTML tag. + */ + public function htmlScriptTagJsObjectV3(): string + { + $html = ''; + if ($this->skip_by_ip) { + $html .= ""; + return $html; + } + + $html .= ""; + + return $html; + } + + /*** + * The same as htmlScriptTagJsObjectV3 but it loads the reCAPTCHA script if the user is not skipped by IP. + * Can be used if you only want to include on specific pages but not send on page load. + * + * @return string + */ + public function htmlScriptTagJsObjectV3WithDependency(): string + { + $html = ''; + if (!$this->skip_by_ip) { + $html = ""; + return $html; + } + $html .= $this->htmlScriptTagJsObjectV3(); + + return $html; + } + } diff --git a/src/ReCaptchaServiceProvider.php b/src/ReCaptchaServiceProvider.php index f7b2e96..d83faeb 100755 --- a/src/ReCaptchaServiceProvider.php +++ b/src/ReCaptchaServiceProvider.php @@ -14,6 +14,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Carbon; /** * Class ReCaptchaServiceProvider @@ -52,10 +53,46 @@ public function addValidationRule() if (!config('recaptcha.empty_message')) { $message = trans(config('recaptcha.error_message_key')); } - Validator::extendImplicit(recaptchaRuleName(), function ($attribute, $value) { - return app('recaptcha')->validate($value); - }, $message); + switch (config('recaptcha.version')) { + case 'v3': + Validator::extendImplicit(recaptchaRuleName(), function ($attribute, $value, $parameters) { + $threshold = floatval($parameters[0] ?? 0.5); + $action = $parameters[1] ?? ''; + + $response = app('recaptcha')->validate($value); + // info("recaptcha response is: ".json_encode($response)); + if (isset($response['skip_by_ip']) && filled($response['skip_by_ip'])) { + return true; + } + + // Verify action if present. + if (filled($action) && isset($response['action']) && $response['action'] !== $action) { + // info("recaptcha action verification failed"); + return false; + } + + // Verify that challenge_ts is within the last 2 minutes if present. + if (isset($response['challenge_ts']) && filled($response['challenge_ts'])) { + $challengeTimestamp = Carbon::parse($response['challenge_ts']); + $currentTimestamp = Carbon::now(); + if ($challengeTimestamp->diffInMinutes($currentTimestamp) > 2) { + // info("recaptcha challenge_ts verification failed"); + return false; + } + } + + return (isset($response['success']) && isset($response['score']) && $response['success'] + && $response['score'] >= $threshold); + }, $message); + break; + case 'v2': + case 'invisible': + Validator::extendImplicit(recaptchaRuleName(), function ($attribute, $value) { + return app('recaptcha')->validate($value); + }, $message); + break; + } } /** diff --git a/src/helpers.php b/src/helpers.php index 51320b3..788ec1e 100755 --- a/src/helpers.php +++ b/src/helpers.php @@ -43,6 +43,41 @@ function htmlScriptTagJsApi(?array $config = []): string } } + +if (!function_exists('htmlScriptTagJsObjectV3')) { + /** + * Writes a HTML script tag that exposes a ReCaptchaV3 object for resolving the reCAPTCHA token. + * Insert this before the closing tag, following the htmlScriptTagJsApi call, as it does not load the reCAPTCHA script. + * + * The ReCaptchaV3 object in JavaScript has a method called execute that returns a promise resolving with a reCAPTCHA token. + * - action: string, defaults to 'homepage'. + * You may set this to a specific action, such as "contact_form_submit", based on the user's action. + * + * Note: This is only valid for v3. + * + * @return string The generated script HTML tag. + */ + function htmlScriptTagJsObjectV3(): string + { + return ReCaptcha::htmlScriptTagJsObjectV3(); + } +} + + +if (!function_exists('htmlScriptTagJsObjectV3WithDependency')) { + /*** + * The same as htmlScriptTagJsObjectV3 but it loads the reCAPTCHA script if the user is not skipped by IP. + * Can be used if you only want to include on specific pages but not send on page load. + * + * @return string + */ + function htmlScriptTagJsObjectV3WithDependency(): string + { + return ReCaptcha::htmlScriptTagJsObjectV3WithDependency(); + } +} + + /** * call ReCaptcha::htmlFormButton() * Write HTML