Skip to content

Commit

Permalink
Server-side validation for v3 recaptcha response
Browse files Browse the repository at this point in the history
fix biscolab#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
  • Loading branch information
LiamKarlMitchell committed Jul 11, 2023
1 parent 1412366 commit b295c67
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/Facades/ReCaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
29 changes: 28 additions & 1 deletion src/ReCaptchaBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 </head> 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 '<!-- htmlScriptTagJsObjectV3 is not implemented for other versions. -->';
}

/***
* 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 '<!-- htmlScriptTagJsObjectV3WithDependency is not implemented for other versions. -->';
}

/**
* Call out to reCAPTCHA and process the response
*
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
60 changes: 60 additions & 0 deletions src/ReCaptchaBuilderV3.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public function getValidationUrlWithToken(): string
* Write script HTML tag in you HTML code
* Insert before </head> tag
*
* I suspect that this is used to inform reCAPTCHA about the page load.
*
* @param array|null $configuration
*
* @return string
Expand Down Expand Up @@ -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 </head> 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 .= "<script>
ReCaptchaV3 = {
execute: async (action = 'homepage') => return 'skip_by_ip'
};
</script>";
return $html;
}

$html .= "<script>
ReCaptchaV3 = {
execute: async (action = 'homepage') => {
return new Promise((resolve, reject) => {
grecaptcha.ready(function() {
grecaptcha.execute('{$this->api_site_key}', {action: action})
.then(token => resolve(token))
.catch(err => reject(err));
})
});
}
};
</script>";

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 = "<script src=\"".$this->api_js_url."?render={$this->api_site_key}\"></script>";
return $html;
}
$html .= $this->htmlScriptTagJsObjectV3();

return $html;
}

}
43 changes: 40 additions & 3 deletions src/ReCaptchaServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Carbon;

/**
* Class ReCaptchaServiceProvider
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
35 changes: 35 additions & 0 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 </head> 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 <button> tag in your HTML code
Expand Down

0 comments on commit b295c67

Please sign in to comment.