From f74c01ca4a9df7edf381154369dc653f5653e630 Mon Sep 17 00:00:00 2001 From: David Coutadeur Date: Wed, 3 Jul 2024 17:03:16 +0200 Subject: [PATCH] add ReCaptcha v3 support (#343) --- conf/config.inc.php | 6 ++ docs/config_general.rst | 15 +++- lib/captcha/ReCaptcha.php | 137 ++++++++++++++++++++++++++++++++ tests/ReCaptchaTest.php | 162 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 lib/captcha/ReCaptcha.php create mode 100644 tests/ReCaptchaTest.php diff --git a/conf/config.inc.php b/conf/config.inc.php index 279103cd..fae0d76c 100644 --- a/conf/config.inc.php +++ b/conf/config.inc.php @@ -375,6 +375,12 @@ #$friendlycaptcha_sitekey = "secret"; #$friendlycaptcha_secret = "secret"; +#$captcha_class = "ReCaptcha"; +#$recaptcha_url = "https://www.google.com/recaptcha/api/siteverify"; +#$recaptcha_sitekey = "sitekey"; +#$recaptcha_secretkey = "secretkey"; +#$recaptcha_minscore = 0.5; + ## Default action # change # sendtoken diff --git a/docs/config_general.rst b/docs/config_general.rst index dd8b9981..675fcc42 100644 --- a/docs/config_general.rst +++ b/docs/config_general.rst @@ -286,7 +286,20 @@ You should also define the captcha module to use. .. tip:: The captcha is used on every form in Self Service Password (password change, token, questions,...) -For ``$captcha_class``, you can select another captcha module. For now, only ``InternalCaptcha`` and ``FriendlyCaptcha`` are supported. +For ``$captcha_class``, you can select another captcha module. For now, only ``InternalCaptcha``, ``FriendlyCaptcha`` and ``ReCaptcha`` are supported. + +If you want to set up ``ReCaptcha``, you must also configure additional parameters: + +.. code-block:: php + + $use_captcha = true; + $captcha_class = "ReCaptcha"; + $recaptcha_url = "https://www.google.com/recaptcha/api/siteverify"; + $recaptcha_sitekey = "sitekey"; + $recaptcha_secretkey = "secretkey"; + $recaptcha_minscore = 0.5; + +See `ReCaptcha documentation `_ for more information If you want to set up ``FriendlyCaptcha``, you must also configure additional parameters: diff --git a/lib/captcha/ReCaptcha.php b/lib/captcha/ReCaptcha.php new file mode 100644 index 00000000..d7220296 --- /dev/null +++ b/lib/captcha/ReCaptcha.php @@ -0,0 +1,137 @@ +recaptcha_url = $recaptcha_url; + $this->recaptcha_sitekey = $recaptcha_sitekey; + $this->recaptcha_secretkey = $recaptcha_secretkey; + $this->recaptcha_minscore = $recaptcha_minscore; + } + + # Function that insert extra css + function generate_css_captcha(){ + $captcha_css = ''; + + return $captcha_css; + } + + # Function that insert extra js + function generate_js_captcha(){ + $captcha_js = ' + + +'; + + return $captcha_js; + } + + # Function that generate the html part containing the captcha + function generate_html_captcha($messages){ + + $captcha_html =' +
+
+
+
+
+ +
+
+
'; + + return $captcha_html; + } + + # Function that generate the captcha challenge + # Could be called by the backend, or by a call through a REST API to define + function generate_captcha_challenge(){ + + $captcha_challenge = ""; + + return $captcha_challenge; + } + + # Function that verify that the result sent by the user + # matches the captcha challenge + function verify_captcha_challenge(){ + $result=""; + if (isset($_POST["captchaphrase"]) and $_POST["captchaphrase"]) { + $captchaphrase = strval($_POST["captchaphrase"]); + + # Call to recaptcha rest api + $data = [ + 'secret' => $this->recaptcha_secretkey, + 'response' => "$captchaphrase" + ]; + $options = [ + 'http' => [ + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($data), + ], + ]; + $context = stream_context_create($options); + $response = file_get_contents($this->recaptcha_url, false, $context); + if ($response === false) { + error_log("Error while reaching ".$this->recaptcha_url); + $result = "badcaptcha"; + } + $json_response = json_decode($response); + if( $json_response->success != "true" ) + { + error_log("Error while verifying captcha $captchaphrase on ".$this->recaptcha_url.": ".var_export($json_response, true)); + $result = "badcaptcha"; + } + else + { + if( !isset($json_response->score) || + $json_response->score < $this->recaptcha_minscore ) + { + error_log("Insufficient score: ".$json_response->score." but minimum required: ".$this->recaptcha_minscore." while verifying captcha $captchaphrase on ".$this->recaptcha_url.": ".var_export($json_response, true)); + $result = "badcaptcha"; + } + else + { + // captcha verified successfully + error_log("Captcha verified successfully: $captchaphrase on ".$this->recaptcha_url.": ".var_export($json_response, true)); + } + } + + } + else { + $result = "captcharequired"; + } + return $result; + } + +} + + +?> diff --git a/tests/ReCaptchaTest.php b/tests/ReCaptchaTest.php new file mode 100644 index 00000000..ec0cf6e5 --- /dev/null +++ b/tests/ReCaptchaTest.php @@ -0,0 +1,162 @@ +assertEquals('captcha\ReCaptcha', get_class($captchaInstance), "Wrong class"); + } + + public function test_generate_js_captcha(): void + { + $recaptcha_url = 'http://127.0.0.1/'; + $recaptcha_sitekey = 'sitekey'; + $recaptcha_secretkey = 'secret'; + $recaptcha_minscore = 0.5; + + $captchaInstance = new captcha\ReCaptcha($recaptcha_url, + $recaptcha_sitekey, + $recaptcha_secretkey, + $recaptcha_minscore); + + $js = $captchaInstance->generate_js_captcha(); + + $this->assertMatchesRegularExpression('/https:\/\/www.google.com\/recaptcha\/api.js/i',$js, "dummy js code returned"); + } + + public function test_generate_html_captcha(): void + { + $messages = array(); + + $recaptcha_url = 'http://127.0.0.1/'; + $recaptcha_sitekey = 'sitekey'; + $recaptcha_secretkey = 'secret'; + $recaptcha_minscore = 0.5; + + $captchaInstance = new captcha\ReCaptcha($recaptcha_url, + $recaptcha_sitekey, + $recaptcha_secretkey, + $recaptcha_minscore); + + $html = $captchaInstance->generate_html_captcha($messages); + + $this->assertMatchesRegularExpression('/getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + $stream_context_create = $this->getFunctionMock("captcha", "stream_context_create"); + $stream_context_create->expects($this->once())->willReturn("stream_context_create"); + $file_get_contents = $this->getFunctionMock("captcha", "file_get_contents"); + $file_get_contents->expects($this->once())->willReturn($http_response); + + $_POST["captchaphrase"] = "ABCDE"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('',$captcha, "unexpected return response during verify_captcha_challenge"); + } + + public function test_verify_captcha_challenge_badcaptcha(): void + { + + $recaptcha_url = 'http://127.0.0.1/'; + $recaptcha_sitekey = 'sitekey'; + $recaptcha_secretkey = 'secret'; + $recaptcha_minscore = 0.5; + $http_response = '{"success": "false"}'; + + $captchaInstance = new captcha\ReCaptcha($recaptcha_url, + $recaptcha_sitekey, + $recaptcha_secretkey, + $recaptcha_minscore); + + $error_log = $this->getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + $stream_context_create = $this->getFunctionMock("captcha", "stream_context_create"); + $stream_context_create->expects($this->once())->willReturn("stream_context_create"); + $file_get_contents = $this->getFunctionMock("captcha", "file_get_contents"); + $file_get_contents->expects($this->once())->willReturn($http_response); + + $_POST["captchaphrase"] = "ABCDE"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('badcaptcha',$captcha, "unexpected return response during verify_captcha_challenge"); + } + + public function test_verify_captcha_challenge_insufficientscore(): void + { + + $recaptcha_url = 'http://127.0.0.1/'; + $recaptcha_sitekey = 'sitekey'; + $recaptcha_secretkey = 'secret'; + $recaptcha_minscore = 0.5; + $http_response = '{"success": "true", "score": "0.4"}'; + + $captchaInstance = new captcha\ReCaptcha($recaptcha_url, + $recaptcha_sitekey, + $recaptcha_secretkey, + $recaptcha_minscore); + + $error_log = $this->getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + $stream_context_create = $this->getFunctionMock("captcha", "stream_context_create"); + $stream_context_create->expects($this->once())->willReturn("stream_context_create"); + $file_get_contents = $this->getFunctionMock("captcha", "file_get_contents"); + $file_get_contents->expects($this->once())->willReturn($http_response); + + $_POST["captchaphrase"] = "ABCDE"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('badcaptcha',$captcha, "unexpected return response during verify_captcha_challenge"); + } + + public function test_verify_captcha_challenge_nocaptcha(): void + { + + $recaptcha_url = 'http://127.0.0.1/'; + $recaptcha_sitekey = 'sitekey'; + $recaptcha_secretkey = 'secret'; + $recaptcha_minscore = 0.5; + + $captchaInstance = new captcha\ReCaptcha($recaptcha_url, + $recaptcha_sitekey, + $recaptcha_secretkey, + $recaptcha_minscore); + + $error_log = $this->getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + + unset($_POST); + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('captcharequired',$captcha, "unexpected return response during verify_captcha_challenge"); + } + +} +