Skip to content

Commit f74c01c

Browse files
David Coutadeurdavidcoutadeur
David Coutadeur
authored andcommitted
add ReCaptcha v3 support (#343)
1 parent 8098000 commit f74c01c

File tree

4 files changed

+319
-1
lines changed

4 files changed

+319
-1
lines changed

conf/config.inc.php

+6
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@
375375
#$friendlycaptcha_sitekey = "secret";
376376
#$friendlycaptcha_secret = "secret";
377377

378+
#$captcha_class = "ReCaptcha";
379+
#$recaptcha_url = "https://www.google.com/recaptcha/api/siteverify";
380+
#$recaptcha_sitekey = "sitekey";
381+
#$recaptcha_secretkey = "secretkey";
382+
#$recaptcha_minscore = 0.5;
383+
378384
## Default action
379385
# change
380386
# sendtoken

docs/config_general.rst

+14-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,20 @@ You should also define the captcha module to use.
286286
.. tip:: The captcha is used on every form in Self Service Password
287287
(password change, token, questions,...)
288288

289-
For ``$captcha_class``, you can select another captcha module. For now, only ``InternalCaptcha`` and ``FriendlyCaptcha`` are supported.
289+
For ``$captcha_class``, you can select another captcha module. For now, only ``InternalCaptcha``, ``FriendlyCaptcha`` and ``ReCaptcha`` are supported.
290+
291+
If you want to set up ``ReCaptcha``, you must also configure additional parameters:
292+
293+
.. code-block:: php
294+
295+
$use_captcha = true;
296+
$captcha_class = "ReCaptcha";
297+
$recaptcha_url = "https://www.google.com/recaptcha/api/siteverify";
298+
$recaptcha_sitekey = "sitekey";
299+
$recaptcha_secretkey = "secretkey";
300+
$recaptcha_minscore = 0.5;
301+
302+
See `ReCaptcha documentation <https://developers.google.com/recaptcha/docs/v3>`_ for more information
290303

291304
If you want to set up ``FriendlyCaptcha``, you must also configure additional parameters:
292305

lib/captcha/ReCaptcha.php

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php namespace captcha;
2+
3+
require_once(__DIR__."/../../vendor/autoload.php");
4+
5+
class ReCaptcha
6+
{
7+
8+
private $recaptcha_url;
9+
private $recaptcha_sitekey;
10+
private $recaptcha_secretkey;
11+
private $recaptcha_minscore;
12+
13+
public function __construct($recaptcha_url, $recaptcha_sitekey, $recaptcha_secretkey, $recaptcha_minscore)
14+
{
15+
$this->recaptcha_url = $recaptcha_url;
16+
$this->recaptcha_sitekey = $recaptcha_sitekey;
17+
$this->recaptcha_secretkey = $recaptcha_secretkey;
18+
$this->recaptcha_minscore = $recaptcha_minscore;
19+
}
20+
21+
# Function that insert extra css
22+
function generate_css_captcha(){
23+
$captcha_css = '';
24+
25+
return $captcha_css;
26+
}
27+
28+
# Function that insert extra js
29+
function generate_js_captcha(){
30+
$captcha_js = '
31+
<script src="https://www.google.com/recaptcha/api.js?render='.$this->recaptcha_sitekey.'"></script>
32+
<script>
33+
$(document).ready(function(){
34+
35+
$(\'button[type="submit"]\').on("click", function (event) {
36+
// only run captcha check and send form if form is valid
37+
if( $("form")[0].checkValidity() )
38+
{
39+
// do not allow to send form before we get the token
40+
event.preventDefault();
41+
grecaptcha.execute("'.$this->recaptcha_sitekey.'", {action: "submit"}).then(function(token) {
42+
// store the token into hidden input in the form
43+
$("#captchaphrase").val(token);
44+
$("form").submit(); // send form
45+
});
46+
}
47+
});
48+
});
49+
</script>
50+
';
51+
52+
return $captcha_js;
53+
}
54+
55+
# Function that generate the html part containing the captcha
56+
function generate_html_captcha($messages){
57+
58+
$captcha_html ='
59+
<div class="row mb-3">
60+
<div class="col-sm-4 col-form-label text-end captcha">
61+
</div>
62+
<div class="col-sm-8">
63+
<div class="input-group">
64+
<input type="hidden" autocomplete="new-password" name="captchaphrase" id="captchaphrase" class="form-control" />
65+
</div>
66+
</div>
67+
</div>';
68+
69+
return $captcha_html;
70+
}
71+
72+
# Function that generate the captcha challenge
73+
# Could be called by the backend, or by a call through a REST API to define
74+
function generate_captcha_challenge(){
75+
76+
$captcha_challenge = "";
77+
78+
return $captcha_challenge;
79+
}
80+
81+
# Function that verify that the result sent by the user
82+
# matches the captcha challenge
83+
function verify_captcha_challenge(){
84+
$result="";
85+
if (isset($_POST["captchaphrase"]) and $_POST["captchaphrase"]) {
86+
$captchaphrase = strval($_POST["captchaphrase"]);
87+
88+
# Call to recaptcha rest api
89+
$data = [
90+
'secret' => $this->recaptcha_secretkey,
91+
'response' => "$captchaphrase"
92+
];
93+
$options = [
94+
'http' => [
95+
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
96+
'method' => 'POST',
97+
'content' => http_build_query($data),
98+
],
99+
];
100+
$context = stream_context_create($options);
101+
$response = file_get_contents($this->recaptcha_url, false, $context);
102+
if ($response === false) {
103+
error_log("Error while reaching ".$this->recaptcha_url);
104+
$result = "badcaptcha";
105+
}
106+
$json_response = json_decode($response);
107+
if( $json_response->success != "true" )
108+
{
109+
error_log("Error while verifying captcha $captchaphrase on ".$this->recaptcha_url.": ".var_export($json_response, true));
110+
$result = "badcaptcha";
111+
}
112+
else
113+
{
114+
if( !isset($json_response->score) ||
115+
$json_response->score < $this->recaptcha_minscore )
116+
{
117+
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));
118+
$result = "badcaptcha";
119+
}
120+
else
121+
{
122+
// captcha verified successfully
123+
error_log("Captcha verified successfully: $captchaphrase on ".$this->recaptcha_url.": ".var_export($json_response, true));
124+
}
125+
}
126+
127+
}
128+
else {
129+
$result = "captcharequired";
130+
}
131+
return $result;
132+
}
133+
134+
}
135+
136+
137+
?>

tests/ReCaptchaTest.php

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../vendor/autoload.php';
4+
require_once __DIR__ . '/../lib/captcha/ReCaptcha.php';
5+
6+
class ReCaptchaTest extends \PHPUnit\Framework\TestCase
7+
{
8+
9+
use \phpmock\phpunit\PHPMock;
10+
11+
public function test_construct(): void
12+
{
13+
$recaptcha_url = 'http://127.0.0.1/';
14+
$recaptcha_sitekey = 'sitekey';
15+
$recaptcha_secretkey = 'secret';
16+
$recaptcha_minscore = 0.5;
17+
18+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
19+
$recaptcha_sitekey,
20+
$recaptcha_secretkey,
21+
$recaptcha_minscore);
22+
23+
$this->assertEquals('captcha\ReCaptcha', get_class($captchaInstance), "Wrong class");
24+
}
25+
26+
public function test_generate_js_captcha(): void
27+
{
28+
$recaptcha_url = 'http://127.0.0.1/';
29+
$recaptcha_sitekey = 'sitekey';
30+
$recaptcha_secretkey = 'secret';
31+
$recaptcha_minscore = 0.5;
32+
33+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
34+
$recaptcha_sitekey,
35+
$recaptcha_secretkey,
36+
$recaptcha_minscore);
37+
38+
$js = $captchaInstance->generate_js_captcha();
39+
40+
$this->assertMatchesRegularExpression('/https:\/\/www.google.com\/recaptcha\/api.js/i',$js, "dummy js code returned");
41+
}
42+
43+
public function test_generate_html_captcha(): void
44+
{
45+
$messages = array();
46+
47+
$recaptcha_url = 'http://127.0.0.1/';
48+
$recaptcha_sitekey = 'sitekey';
49+
$recaptcha_secretkey = 'secret';
50+
$recaptcha_minscore = 0.5;
51+
52+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
53+
$recaptcha_sitekey,
54+
$recaptcha_secretkey,
55+
$recaptcha_minscore);
56+
57+
$html = $captchaInstance->generate_html_captcha($messages);
58+
59+
$this->assertMatchesRegularExpression('/<input type="hidden" autocomplete="new-password" name="captchaphrase" id="captchaphrase" class="form-control"/',$html, "dummy challenge in html code");
60+
}
61+
62+
public function test_verify_captcha_challenge_ok(): void
63+
{
64+
65+
$recaptcha_url = 'http://127.0.0.1/';
66+
$recaptcha_sitekey = 'sitekey';
67+
$recaptcha_secretkey = 'secret';
68+
$recaptcha_minscore = 0.5;
69+
$http_response = '{"success": "true", "score": "0.9"}';
70+
71+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
72+
$recaptcha_sitekey,
73+
$recaptcha_secretkey,
74+
$recaptcha_minscore);
75+
76+
$error_log = $this->getFunctionMock("captcha", "error_log");
77+
$error_log->expects($this->any())->willReturn("");
78+
$stream_context_create = $this->getFunctionMock("captcha", "stream_context_create");
79+
$stream_context_create->expects($this->once())->willReturn("stream_context_create");
80+
$file_get_contents = $this->getFunctionMock("captcha", "file_get_contents");
81+
$file_get_contents->expects($this->once())->willReturn($http_response);
82+
83+
$_POST["captchaphrase"] = "ABCDE";
84+
$captcha = $captchaInstance->verify_captcha_challenge();
85+
$this->assertEquals('',$captcha, "unexpected return response during verify_captcha_challenge");
86+
}
87+
88+
public function test_verify_captcha_challenge_badcaptcha(): void
89+
{
90+
91+
$recaptcha_url = 'http://127.0.0.1/';
92+
$recaptcha_sitekey = 'sitekey';
93+
$recaptcha_secretkey = 'secret';
94+
$recaptcha_minscore = 0.5;
95+
$http_response = '{"success": "false"}';
96+
97+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
98+
$recaptcha_sitekey,
99+
$recaptcha_secretkey,
100+
$recaptcha_minscore);
101+
102+
$error_log = $this->getFunctionMock("captcha", "error_log");
103+
$error_log->expects($this->any())->willReturn("");
104+
$stream_context_create = $this->getFunctionMock("captcha", "stream_context_create");
105+
$stream_context_create->expects($this->once())->willReturn("stream_context_create");
106+
$file_get_contents = $this->getFunctionMock("captcha", "file_get_contents");
107+
$file_get_contents->expects($this->once())->willReturn($http_response);
108+
109+
$_POST["captchaphrase"] = "ABCDE";
110+
$captcha = $captchaInstance->verify_captcha_challenge();
111+
$this->assertEquals('badcaptcha',$captcha, "unexpected return response during verify_captcha_challenge");
112+
}
113+
114+
public function test_verify_captcha_challenge_insufficientscore(): void
115+
{
116+
117+
$recaptcha_url = 'http://127.0.0.1/';
118+
$recaptcha_sitekey = 'sitekey';
119+
$recaptcha_secretkey = 'secret';
120+
$recaptcha_minscore = 0.5;
121+
$http_response = '{"success": "true", "score": "0.4"}';
122+
123+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
124+
$recaptcha_sitekey,
125+
$recaptcha_secretkey,
126+
$recaptcha_minscore);
127+
128+
$error_log = $this->getFunctionMock("captcha", "error_log");
129+
$error_log->expects($this->any())->willReturn("");
130+
$stream_context_create = $this->getFunctionMock("captcha", "stream_context_create");
131+
$stream_context_create->expects($this->once())->willReturn("stream_context_create");
132+
$file_get_contents = $this->getFunctionMock("captcha", "file_get_contents");
133+
$file_get_contents->expects($this->once())->willReturn($http_response);
134+
135+
$_POST["captchaphrase"] = "ABCDE";
136+
$captcha = $captchaInstance->verify_captcha_challenge();
137+
$this->assertEquals('badcaptcha',$captcha, "unexpected return response during verify_captcha_challenge");
138+
}
139+
140+
public function test_verify_captcha_challenge_nocaptcha(): void
141+
{
142+
143+
$recaptcha_url = 'http://127.0.0.1/';
144+
$recaptcha_sitekey = 'sitekey';
145+
$recaptcha_secretkey = 'secret';
146+
$recaptcha_minscore = 0.5;
147+
148+
$captchaInstance = new captcha\ReCaptcha($recaptcha_url,
149+
$recaptcha_sitekey,
150+
$recaptcha_secretkey,
151+
$recaptcha_minscore);
152+
153+
$error_log = $this->getFunctionMock("captcha", "error_log");
154+
$error_log->expects($this->any())->willReturn("");
155+
156+
unset($_POST);
157+
$captcha = $captchaInstance->verify_captcha_challenge();
158+
$this->assertEquals('captcharequired',$captcha, "unexpected return response during verify_captcha_challenge");
159+
}
160+
161+
}
162+

0 commit comments

Comments
 (0)