diff --git a/conf/config.inc.php b/conf/config.inc.php index f037b076..9613c310 100644 --- a/conf/config.inc.php +++ b/conf/config.inc.php @@ -368,6 +368,7 @@ ## Captcha $use_captcha = false; +$captcha_class = "InternalCaptcha"; ## Default action # change diff --git a/docs/config_general.rst b/docs/config_general.rst index 8d45b849..664bc92a 100644 --- a/docs/config_general.rst +++ b/docs/config_general.rst @@ -268,17 +268,28 @@ GET or POST parameter. This method does not require any configuration. Example: ``https://ssp.example.com/?actionresetbyquestions&login_hint=spiderman`` +.. _config_captcha: + Captcha ------- -To require a captcha, set ``$use_captcha``: +To enable captcha, set ``$use_captcha`` to ``true``. + +You should also define the captcha module to use. +(By default, ``InternalCaptcha`` is defined in config.inc.php) .. code-block:: php $use_captcha = true; + $captcha_class = "InternalCaptcha"; .. tip:: The captcha is used on every form in Self Service Password - (password change, token, questions, etc.) + (password change, token, questions,...) + +For ``$captcha_class``, you can select another captcha module. For now, only ``InternalCaptcha`` and ``FriendlyCaptcha`` are supported. + +You can also add your own Captcha module. (see :doc:`developpers` ) + .. |image0| image:: images/br.png .. |image1| image:: images/catalonia.png diff --git a/docs/developpers.rst b/docs/developpers.rst new file mode 100644 index 00000000..11e1ac3a --- /dev/null +++ b/docs/developpers.rst @@ -0,0 +1,160 @@ +Developper's corner +=================== + +LDAP Tool Box Self Service Password can be extended with your own code. + +Add your own Captcha system +--------------------------- + +As presented in :ref:`captcha configuration`, you can enable a captcha on most of the pages of Self-Service-Password. + +You can define a customized class for managing your own captcha class: + +.. code-block:: php + + $use_captcha = true; + $captcha_class = "MyCustomClass"; + + +Then you have to create the captcha module in ``lib/captcha/MyCustomClass.php``. + +Here is a template example of such a captcha module: + +.. code-block:: php + + captcha_property = $property; + } + + + # 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 =' +
+
+ captcha + +
+
+
+ + +
+
+
'; + + 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(){ + + # cookie for captcha session + ini_set("session.use_cookies",1); + ini_set("session.use_only_cookies",1); + session_name("captcha"); + session_start(); + + # Generate your captcha challenge here + $challenge = ""; + + $_SESSION['phrase'] = $challenge; + + # session is stored and closed now, used only for captcha + session_write_close(); + + $captcha_image = $captcha->build()->inline(); + + return $captcha_image; + } + + # 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"]) { + # captcha cookie for session + ini_set("session.use_cookies",1); + ini_set("session.use_only_cookies",1); + setcookie("captcha", '', time()-1000); + session_name("captcha"); + session_start(); + $captchaphrase = strval($_POST["captchaphrase"]); + + # Compare captcha stored in session and user guess + if (! isset($_SESSION['phrase']) or + $_SESSION['phrase'] != $captchaphrase) { + $result = "badcaptcha"; + } + unset($_SESSION['phrase']); + # write session to make sure captcha phrase is no more included in session. + session_write_close(); + } + else { + $result = "captcharequired"; + } + return $result; + } + + } + + + ?> + + +Points of attention: + +* you can set any configuration parameters in ``config.inc.local.php``, they will be passed to your class if you define them as properties, and initialize them in the constructor +* you can inject extra css in ``generate_css_captcha`` function +* you can inject extra js in ``generate_js_captcha`` function. For example, js code can useful for refreshing the challenge. If so, you are expected to reach ``/newcaptcha.php`` endpoint. This endpoint would call the ``generate_captcha_challenge`` function in current MyCustomClass and returns the result in json format. +* you must fill in the ``generate_html_captcha`` function. This function must return the html code corresponding to the captcha. It should call the ``generate_captcha_challenge``. +* you must fill in the ``generate_captcha_challenge`` function. This function must generate the challenge, and ensure it is stored somewhere (in the php session). This function can also be called by the REST endpoint: ``/newcaptcha.php`` +* you must fill in the ``verify_captcha_challenge`` function. This function must compare the challenge generated and stored, and the user guess. It must return a string corresponding to the status: ``badcaptcha``, ``captcharequired``, or empty string (empty string means challenge is verified) +* don't forget to declare the namespace: ``namespace captcha;`` +* don't forget to write the corresponding unit tests (see tests/InternalCaptchaTest.php) + + +Run unit tests +-------------- + +Run the unit tests with this command: + +``` +XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --configuration tests/phpunit.xml +``` + +Take care to use the phpunit shipped with composer. + +If you don't have the composer dependencies yet: + +``` +composer update +``` + diff --git a/docs/index.rst b/docs/index.rst index 9e33a8de..99c7a5bc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,3 +25,4 @@ LDAP Tool Box Self Service Password documentation webservices.rst audit.rst set_attributes.rst + developpers.rst diff --git a/htdocs/captcha.php b/htdocs/captcha.php deleted file mode 100644 index 39e38393..00000000 --- a/htdocs/captcha.php +++ /dev/null @@ -1,42 +0,0 @@ -getPhrase(); - -# session is stored and closed now, used only for captcha -session_write_close(); - -header('Content-Type: image/jpeg'); -$captcha - ->build() - ->output() -; -?> diff --git a/htdocs/change.php b/htdocs/change.php index e01b7227..9a4039c8 100644 --- a/htdocs/change.php +++ b/htdocs/change.php @@ -59,7 +59,7 @@ #============================================================================== # Check captcha #============================================================================== -if ( ( $result === "" ) and $use_captcha) { $result = global_captcha_check();} +if ( ( $result === "" ) and $use_captcha) { $result = $captchaInstance->verify_captcha_challenge();} #============================================================================== # Check old password diff --git a/htdocs/changecustompwdfield.php b/htdocs/changecustompwdfield.php index 7e91042a..a88f591e 100644 --- a/htdocs/changecustompwdfield.php +++ b/htdocs/changecustompwdfield.php @@ -84,7 +84,7 @@ function set_default_value(&$variable, $defaultValue) #============================================================================== # Check captcha #============================================================================== -if ( ( $result === "" ) and $use_captcha ) { $result = global_captcha_check();} +if ( ( $result === "" ) and $use_captcha ) { $result = $captchaInstance->verify_captcha_challenge();} #============================================================================== # Default configuration diff --git a/htdocs/changesshkey.php b/htdocs/changesshkey.php index 6532a948..2a9af68a 100644 --- a/htdocs/changesshkey.php +++ b/htdocs/changesshkey.php @@ -53,7 +53,7 @@ #============================================================================== # Check captcha #============================================================================== -if ( ( $result === "" ) and $use_captcha) { $result = global_captcha_check();} +if ( ( $result === "" ) and $use_captcha) { $result = $captchaInstance->verify_captcha_challenge();} #============================================================================== # Check password diff --git a/htdocs/index.php b/htdocs/index.php index 39608dd2..d0a42390 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -15,9 +15,6 @@ #============================================================================== require_once("../vendor/autoload.php"); require_once("../lib/functions.inc.php"); -if ($use_captcha) { - require_once("../lib/captcha.inc.php"); -} #============================================================================== # VARIABLES @@ -122,6 +119,11 @@ isset($ldap_krb5ccname) ? $ldap_krb5ccname : null ); +#============================================================================== +# Captcha Config +#============================================================================== +require_once(__DIR__ . "/../lib/captcha.inc.php"); + #============================================================================== # Other default values #============================================================================== @@ -209,6 +211,19 @@ auditlog($audit_log_file, $userdn, $login, $action, $result); } +#============================================================================== +# Generate captcha +#============================================================================== +$captcha_html = ''; +$captcha_js = ''; +$captcha_css = ''; +if(isset($use_captcha) && $use_captcha == true) +{ + $captcha_html = $captchaInstance->generate_html_captcha($messages); + $captcha_js = $captchaInstance->generate_js_captcha(); + $captcha_css = $captchaInstance->generate_css_captcha(); +} + #============================================================================== # Smarty #============================================================================== @@ -266,6 +281,9 @@ $smarty->assign('mail_address_use_ldap', $mail_address_use_ldap); $smarty->assign('sms_use_ldap', $sms_use_ldap); $smarty->assign('default_action', $default_action); +$smarty->assign('captcha_html', $captcha_html); +$smarty->assign('captcha_js', $captcha_js); +$smarty->assign('captcha_css', $captcha_css); //$smarty->assign('',); if (isset($source)) { $smarty->assign('source', $source); } diff --git a/htdocs/newcaptcha.php b/htdocs/newcaptcha.php new file mode 100644 index 00000000..4e8c33f4 --- /dev/null +++ b/htdocs/newcaptcha.php @@ -0,0 +1,17 @@ +generate_captcha_challenge(); +$result = array( + 'challenge' => "$captcha_challenge", +); + +header('Content-type: application/json'); +echo json_encode($result); + +?> diff --git a/htdocs/resetbyquestions.php b/htdocs/resetbyquestions.php index a994e34a..b1ee9fbd 100644 --- a/htdocs/resetbyquestions.php +++ b/htdocs/resetbyquestions.php @@ -84,7 +84,7 @@ #============================================================================== # Check captcha #============================================================================== -if ( ( $result === "" ) and $use_captcha) { $result = global_captcha_check();} +if ( ( $result === "" ) and $use_captcha) { $result = $captchaInstance->verify_captcha_challenge();} # Should we pre-populate the question? # This should ensure that $login is valid and everything else is empty. diff --git a/htdocs/sendsms.php b/htdocs/sendsms.php index 82428088..968eb396 100644 --- a/htdocs/sendsms.php +++ b/htdocs/sendsms.php @@ -148,7 +148,7 @@ # Check captcha #============================================================================== if ( $result === "" and $use_captcha) { - $result = global_captcha_check(); + $result = $captchaInstance->verify_captcha_challenge(); } #============================================================================== diff --git a/htdocs/sendtoken.php b/htdocs/sendtoken.php index 67c37a48..e9f0ada3 100644 --- a/htdocs/sendtoken.php +++ b/htdocs/sendtoken.php @@ -59,7 +59,7 @@ # Check captcha #============================================================================== if ( ( $result === "" ) and $use_captcha) { - $result = global_captcha_check(); + $result = $captchaInstance->verify_captcha_challenge(); } #============================================================================== diff --git a/htdocs/setattributes.php b/htdocs/setattributes.php index 02094080..a464ed44 100644 --- a/htdocs/setattributes.php +++ b/htdocs/setattributes.php @@ -51,7 +51,7 @@ #============================================================================== # Check captcha #============================================================================== -if ( ( $result === "" ) and $use_captcha) { $result = global_captcha_check();} +if ( ( $result === "" ) and $use_captcha) { $result = $captchaInstance->verify_captcha_challenge();} #============================================================================== # Check password diff --git a/htdocs/setquestions.php b/htdocs/setquestions.php index 083f6095..4fa1c274 100644 --- a/htdocs/setquestions.php +++ b/htdocs/setquestions.php @@ -74,7 +74,7 @@ #============================================================================== # Check captcha #============================================================================== -if ( ( $result === "" ) and $use_captcha) { $result = global_captcha_check();} +if ( ( $result === "" ) and $use_captcha) { $result = $captchaInstance->verify_captcha_challenge();} #============================================================================== # Check password diff --git a/lib/captcha.inc.php b/lib/captcha.inc.php index 6d55992d..5742fa87 100644 --- a/lib/captcha.inc.php +++ b/lib/captcha.inc.php @@ -1,52 +1,39 @@ getConstructor()->getParameters(); -# see ../htdocs/captcha.php where captcha cookie and $_SESSION['phrase'] are set. -function global_captcha_check() { - $result=""; - if (isset($_POST["captchaphrase"]) and $_POST["captchaphrase"]) { - # captcha cookie for session - ini_set("session.use_cookies",1); - ini_set("session.use_only_cookies",1); - setcookie("captcha", '', time()-1000); - session_name("captcha"); - session_start(); - $captchaphrase = strval($_POST["captchaphrase"]); - if (!isset($_SESSION['phrase']) or !check_captcha($_SESSION['phrase'], $captchaphrase)) { - $result = "badcaptcha"; + # Gather parameters to pass to the class: all config params to pass + $definedVariables = get_defined_vars(); # get all variables, including configuration + $params = []; + foreach ($constructorParams AS $param) { + if(!isset($definedVariables[$param->name])) + { + error_log("Error: Missing param $param->name for $captcha_class"); + exit(1); + } + array_push($params, $definedVariables[$param->name]); } - unset($_SESSION['phrase']); - # write session to make sure captcha phrase is no more included in session. - session_write_close(); + + $captchaInstance = new $captcha_fullclass(...$params); } - else { - $result = "captcharequired"; + else + { + error_log("Error: unable to load captcha class $captcha_class in " . + __DIR__ . "/../lib/captcha/" . $captcha_class . ".php"); + exit(1); } - return $result; } + +?> diff --git a/lib/captcha/InternalCaptcha.php b/lib/captcha/InternalCaptcha.php new file mode 100644 index 00000000..29a477eb --- /dev/null +++ b/lib/captcha/InternalCaptcha.php @@ -0,0 +1,135 @@ +captcha_property = $property; + } + + # Function that insert extra css + function generate_css_captcha(){ + $captcha_css = ' + #captcha-refresh{ + margin-left: 10px; + } + '; + + 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 =' +
+
+ captcha + +
+
+
+ + +
+
+
'; + + 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(){ + + # cookie for captcha session + ini_set("session.use_cookies",1); + ini_set("session.use_only_cookies",1); + session_name("captcha"); + session_start(); + + $captcha = new CaptchaBuilder; + + $_SESSION['phrase'] = $captcha->getPhrase(); + + # session is stored and closed now, used only for captcha + session_write_close(); + + $captcha_image = $captcha->build()->inline(); + + return $captcha_image; + } + + # 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"]) { + # captcha cookie for session + ini_set("session.use_cookies",1); + ini_set("session.use_only_cookies",1); + setcookie("captcha", '', time()-1000); + session_name("captcha"); + session_start(); + $captchaphrase = strval($_POST["captchaphrase"]); + if (! isset($_SESSION['phrase']) or + ! PhraseBuilder::comparePhrases($_SESSION['phrase'], $captchaphrase)) { + $result = "badcaptcha"; + } + unset($_SESSION['phrase']); + # write session to make sure captcha phrase is no more included in session. + session_write_close(); + } + else { + $result = "captcharequired"; + } + return $result; + } + +} + + +?> diff --git a/templates/change.tpl b/templates/change.tpl index 8c975989..261f1289 100644 --- a/templates/change.tpl +++ b/templates/change.tpl @@ -75,7 +75,7 @@ {if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/templates/changecustompwdfield.tpl b/templates/changecustompwdfield.tpl index 6ef22a29..8910bf57 100644 --- a/templates/changecustompwdfield.tpl +++ b/templates/changecustompwdfield.tpl @@ -76,7 +76,7 @@
{if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/templates/changesshkey.tpl b/templates/changesshkey.tpl index b84a9420..d8218c4c 100644 --- a/templates/changesshkey.tpl +++ b/templates/changesshkey.tpl @@ -34,7 +34,7 @@
{if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/templates/footer.tpl b/templates/footer.tpl index c8e732d8..8e97b672 100644 --- a/templates/footer.tpl +++ b/templates/footer.tpl @@ -12,6 +12,9 @@ policy = JSON.parse(atob(json_policy)); +{if $captcha_js} +{$captcha_js nofilter} +{/if} {if ($questions_count > 1)} diff --git a/templates/header.tpl b/templates/header.tpl index 840346be..4bdd29bc 100644 --- a/templates/header.tpl +++ b/templates/header.tpl @@ -24,6 +24,9 @@ } {/if} +{if $captcha_css} + +{/if}
diff --git a/templates/resetbyquestions.tpl b/templates/resetbyquestions.tpl index 1553265f..3e11bbd1 100644 --- a/templates/resetbyquestions.tpl +++ b/templates/resetbyquestions.tpl @@ -101,7 +101,7 @@
{if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/templates/sendsms.tpl b/templates/sendsms.tpl index 03637cd1..5efa846e 100644 --- a/templates/sendsms.tpl +++ b/templates/sendsms.tpl @@ -79,7 +79,7 @@
{if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if} {if !$sms_use_ldap}
diff --git a/templates/sendtoken.tpl b/templates/sendtoken.tpl index 1c5b4b87..9abdbb48 100644 --- a/templates/sendtoken.tpl +++ b/templates/sendtoken.tpl @@ -35,7 +35,7 @@
{/if} {if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/templates/setattributes.tpl b/templates/setattributes.tpl index 9639fb53..d55ad3de 100644 --- a/templates/setattributes.tpl +++ b/templates/setattributes.tpl @@ -49,7 +49,7 @@ {/if} {if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/templates/setquestions.tpl b/templates/setquestions.tpl index 8862ad60..5fcf852d 100644 --- a/templates/setquestions.tpl +++ b/templates/setquestions.tpl @@ -76,7 +76,7 @@
{/if} {if ($use_captcha)} - {include file="captcha.tpl"} + {$captcha_html nofilter} {/if}
diff --git a/tests/InternalCaptchaTest.php b/tests/InternalCaptchaTest.php new file mode 100644 index 00000000..efd84f7e --- /dev/null +++ b/tests/InternalCaptchaTest.php @@ -0,0 +1,107 @@ +assertEquals('captcha\InternalCaptcha', get_class($captchaInstance), "Wrong class"); + } + + public function test_generate_css_captcha(): void + { + $captchaInstance = new captcha\InternalCaptcha(); + + $css = $captchaInstance->generate_css_captcha(); + + $this->assertMatchesRegularExpression('/captcha-refresh/i',$css, "dummy css code returned"); + } + + public function test_generate_js_captcha(): void + { + $captchaInstance = new captcha\InternalCaptcha(); + + $js = $captchaInstance->generate_js_captcha(); + + $this->assertMatchesRegularExpression('/captcha-refresh/i',$js, "dummy js code returned"); + } + + public function test_generate_html_captcha(): void + { + $messages = array("captcha" => "Captcha"); + + $captchaMock = $this->getMockBuilder(captcha\InternalCaptcha::class) + ->onlyMethods(['generate_captcha_challenge']) + ->getMock(); + $captchaMock->expects($this->once()) + ->method('generate_captcha_challenge') + ->will($this->returnValue("my-challenge")); + + $html = $captchaMock->generate_html_captcha($messages); + + $this->assertMatchesRegularExpression('/assertMatchesRegularExpression('/getFunctionMock("captcha", "ini_set"); + $ini_set->expects($this->any())->willReturn("dummy"); + $session_name = $this->getFunctionMock("captcha", "session_name"); + $session_name->expects($this->any())->willReturn("dummy"); + $session_start = $this->getFunctionMock("captcha", "session_start"); + $session_start->expects($this->any())->willReturn("dummy"); + + $captchaInstance = new captcha\InternalCaptcha(); + + $captcha = $captchaInstance->generate_captcha_challenge(); + + $this->assertMatchesRegularExpression('/^data:image\/jpeg;base64,/',$captcha, "dummy challenge image"); + $this->assertMatchesRegularExpression('/^[A-Za-z0-9]{5}$/',$_SESSION['phrase'], "dummy challenge text"); + + } + + public function test_verify_captcha_challenge(): void + { + + $_POST["captchaphrase"] = "ABCDE"; + $_SESSION['phrase'] = "ABCDE"; + + $ini_set = $this->getFunctionMock("captcha", "ini_set"); + $ini_set->expects($this->any())->willReturn("dummy"); + $setcookie = $this->getFunctionMock("captcha", "setcookie"); + $setcookie->expects($this->any())->willReturn("dummy"); + $session_name = $this->getFunctionMock("captcha", "session_name"); + $session_name->expects($this->any())->willReturn("dummy"); + $session_start = $this->getFunctionMock("captcha", "session_start"); + $session_start->expects($this->any())->willReturn("dummy"); + + $captchaInstance = new captcha\InternalCaptcha(); + + # Test captcha == phrase : result OK + $_POST["captchaphrase"] = "ABCDE"; + $_SESSION['phrase'] = "ABCDE"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('',$captcha, "unexpected return response during verify_captcha_challenge"); + + # Test captcha != phrase : result KO + $_POST["captchaphrase"] = "ABCDE"; + $_SESSION['phrase'] = "12345"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('badcaptcha',$captcha, "unexpected return response during verify_captcha_challenge"); + + # Test no captcha present : result KO + unset($_POST); + $_SESSION['phrase'] = "12345"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('captcharequired',$captcha, "unexpected return response during verify_captcha_challenge"); + + } +}