MDL-53368 core_auth: Implement reCaptcha on login page
This commit is contained in:
@@ -116,6 +116,17 @@ if ($hassiteconfig) {
|
||||
$temp->add($setting);
|
||||
$temp->add(new admin_setting_configcheckbox('verifychangedemail', new lang_string('verifychangedemail', 'admin'), new lang_string('configverifychangedemail', 'admin'), 1));
|
||||
|
||||
// ReCaptcha.
|
||||
$temp->add(new admin_setting_configselect('enableloginrecaptcha',
|
||||
new lang_string('auth_loginrecaptcha', 'auth'),
|
||||
new lang_string('auth_loginrecaptcha_desc', 'auth'),
|
||||
0,
|
||||
[
|
||||
new lang_string('no'),
|
||||
new lang_string('yes'),
|
||||
],
|
||||
));
|
||||
|
||||
$setting = new admin_setting_configtext('recaptchapublickey', new lang_string('recaptchapublickey', 'admin'), new lang_string('configrecaptchapublickey', 'admin'), '', PARAM_NOTAGS);
|
||||
$setting->set_force_ltr(true);
|
||||
$temp->add($setting);
|
||||
|
||||
@@ -71,6 +71,8 @@ class login implements renderable, templatable {
|
||||
public $logintoken;
|
||||
/** @var string Maintenance message, if Maintenance is enabled. */
|
||||
public $maintenance;
|
||||
/** @var string ReCaptcha element HTML. */
|
||||
public $recaptcha;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -120,6 +122,12 @@ class login implements renderable, templatable {
|
||||
// Identity providers.
|
||||
$this->identityproviders = \auth_plugin_base::get_identity_providers($authsequence);
|
||||
$this->logintoken = \core\session\manager::get_login_token();
|
||||
|
||||
// ReCaptcha.
|
||||
if (login_captcha_enabled()) {
|
||||
require_once($CFG->libdir . '/recaptchalib_v2.php');
|
||||
$this->recaptcha = recaptcha_get_challenge_html(RECAPTCHA_API_URL, $CFG->recaptchapublickey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +162,7 @@ class login implements renderable, templatable {
|
||||
$data->logintoken = $this->logintoken;
|
||||
$data->maintenance = format_text($this->maintenance, FORMAT_MOODLE);
|
||||
$data->languagemenu = $this->languagemenu;
|
||||
$data->recaptcha = $this->recaptcha;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ $string['auth_changepasswordurl_expl'] = 'Specify the url to send users who have
|
||||
$string['auth_changingemailaddress'] = 'You have requested a change of email address, from {$a->oldemail} to {$a->newemail}. For security reasons, we are sending you an email message at the new address to confirm that it belongs to you. Your email address will be updated as soon as you open the URL sent to you in that message.';
|
||||
$string['authinstructions'] = 'Leave this blank for the default login instructions to be displayed on the login page. If you want to provide custom login instructions, enter them here.';
|
||||
$string['auth_invalidnewemailkey'] = 'Error: if you are trying to confirm a change of email address, you may have made a mistake in copying the URL we sent you by email. Please copy the address and try again.';
|
||||
$string['auth_loginrecaptcha'] = 'Enable reCAPTCHA for login';
|
||||
$string['auth_loginrecaptcha_desc'] = 'Add a visual/audio confirmation form element to the login page. This reduces the risk of unwarranted login attempts. See <a target="_blank" href="https://www.google.com/recaptcha">Google reCAPTCHA</a> for more details. ';
|
||||
$string['auth_multiplehosts'] = 'Multiple hosts OR addresses can be specified (eg host1.com;host2.com;host3.com) or (eg xxx.xxx.xxx.xxx;xxx.xxx.xxx.xxx)';
|
||||
$string['auth_notconfigured'] = 'The authentication method {$a} is not configured.';
|
||||
$string['auth_outofnewemailupdateattempts'] = 'You have run out of allowed attempts to update your email address. Your update request has been cancelled.';
|
||||
|
||||
@@ -79,6 +79,9 @@ define('AUTH_LOGIN_LOCKOUT', 4);
|
||||
/** Can not login becauser user is not authorised. */
|
||||
define('AUTH_LOGIN_UNAUTHORISED', 5);
|
||||
|
||||
/** Can not login, failed reCaptcha challenge. */
|
||||
define('AUTH_LOGIN_FAILED_RECAPTCHA', 6);
|
||||
|
||||
/**
|
||||
* Abstract authentication plugin.
|
||||
*
|
||||
@@ -1037,6 +1040,40 @@ function signup_captcha_enabled() {
|
||||
return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && $authplugin->is_captcha_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the captcha element is enabled for the login form, and the admin settings fulfil its requirements.
|
||||
* @return bool
|
||||
*/
|
||||
function login_captcha_enabled(): bool {
|
||||
global $CFG;
|
||||
return !empty($CFG->recaptchapublickey) && !empty($CFG->recaptchaprivatekey) && $CFG->enableloginrecaptcha == true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the submitted captcha is valid or not.
|
||||
*
|
||||
* @param string|bool $captcha The value submitted in the login form that we are validating.
|
||||
* If false is passed for the captcha, this function will always return true.
|
||||
* @return boolean If the submitted captcha is valid.
|
||||
*/
|
||||
function validate_login_captcha(string|bool $captcha): bool {
|
||||
global $CFG;
|
||||
if (!empty($CFG->alternateloginurl)) {
|
||||
// An external login page cannot use the reCaptcha.
|
||||
return true;
|
||||
}
|
||||
if ($captcha === false) {
|
||||
// The authenticate_user_login() is a core function was extended to validate captcha.
|
||||
// For existing uses other than the login form it does not need to validate the captcha.
|
||||
// Example: login/change_password_form.php or login/token.php.
|
||||
return true;
|
||||
}
|
||||
|
||||
require_once($CFG->libdir . '/recaptchalib_v2.php');
|
||||
$response = recaptcha_check_response(RECAPTCHA_VERIFY_URL, $CFG->recaptchaprivatekey, getremoteaddr(), $captcha);
|
||||
return $response['isvalid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the standard sign-up data (except recaptcha that is validated by the form element).
|
||||
*
|
||||
|
||||
+24
-2
@@ -4242,10 +4242,18 @@ function guest_user() {
|
||||
* @param string $password User's password
|
||||
* @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
|
||||
* @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
|
||||
* @param mixed logintoken If this is set to a string it is validated against the login token for the session.
|
||||
* @param string|bool $logintoken If this is set to a string it is validated against the login token for the session.
|
||||
* @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
|
||||
* @return stdClass|false A {@link $USER} object or false if error
|
||||
*/
|
||||
function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
|
||||
function authenticate_user_login(
|
||||
$username,
|
||||
$password,
|
||||
$ignorelockout = false,
|
||||
&$failurereason = null,
|
||||
$logintoken = false,
|
||||
string|bool $loginrecaptcha = false,
|
||||
) {
|
||||
global $CFG, $DB, $PAGE;
|
||||
require_once("$CFG->libdir/authlib.php");
|
||||
|
||||
@@ -4284,6 +4292,20 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
|
||||
return false;
|
||||
}
|
||||
|
||||
// Login reCaptcha.
|
||||
if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) {
|
||||
$failurereason = AUTH_LOGIN_FAILED_RECAPTCHA;
|
||||
// Trigger login failed event (specifying the ID of the found user, if available).
|
||||
\core\event\user_login_failed::create([
|
||||
'userid' => ($user->id ?? 0),
|
||||
'other' => [
|
||||
'username' => $username,
|
||||
'reason' => $failurereason,
|
||||
],
|
||||
])->trigger();
|
||||
return false;
|
||||
}
|
||||
|
||||
$authsenabled = get_enabled_auth_plugins();
|
||||
|
||||
if ($user) {
|
||||
|
||||
@@ -142,6 +142,11 @@
|
||||
!}}placeholder="{{#cleanstr}}password{{/cleanstr}}" {{!
|
||||
!}}autocomplete="current-password">
|
||||
</div>
|
||||
{{#recaptcha}}
|
||||
<div class="login-form-recaptcha form-group">
|
||||
{{{recaptcha}}}
|
||||
</div>
|
||||
{{/recaptcha}}
|
||||
<div class="login-form-submit form-group">
|
||||
<button class="btn btn-primary btn-lg" type="submit" id="loginbtn">{{#str}}login{{/str}}</button>
|
||||
</div>
|
||||
|
||||
@@ -390,6 +390,50 @@ class authlib_test extends \advanced_testcase {
|
||||
$this->assertEquals(count($events), 0);
|
||||
// Check no notifications.
|
||||
$this->assertEquals(count($notifications), 0);
|
||||
|
||||
// Capture failed login reCaptcha.
|
||||
$CFG->recaptchapublickey = 'randompublickey';
|
||||
$CFG->recaptchaprivatekey = 'randomprivatekey';
|
||||
$CFG->enableloginrecaptcha = true;
|
||||
|
||||
// Login with blank captcha.
|
||||
$sink = $this->redirectEvents();
|
||||
$result = authenticate_user_login('username1', 'password1', false, $reason, false, '');
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
$event = array_pop($events);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AUTH_LOGIN_FAILED_RECAPTCHA, $reason);
|
||||
|
||||
// Test event.
|
||||
$this->assertInstanceOf('\core\event\user_login_failed', $event);
|
||||
$eventdata = $event->get_data();
|
||||
$this->assertSame($eventdata['other']['username'], 'username1');
|
||||
$this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED_RECAPTCHA);
|
||||
$this->assertEventContextNotUsed($event);
|
||||
|
||||
// Login with invalid captcha.
|
||||
$sink = $this->redirectEvents();
|
||||
$result = authenticate_user_login('username1', 'password1', false, $reason, false, 'invalidcaptcha');
|
||||
$events = $sink->get_events();
|
||||
$sink->close();
|
||||
$event = array_pop($events);
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertEquals(AUTH_LOGIN_FAILED_RECAPTCHA, $reason);
|
||||
|
||||
// Test event.
|
||||
$this->assertInstanceOf('\core\event\user_login_failed', $event);
|
||||
$eventdata = $event->get_data();
|
||||
$this->assertSame($eventdata['other']['username'], 'username1');
|
||||
$this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED_RECAPTCHA);
|
||||
$this->assertEventContextNotUsed($event);
|
||||
|
||||
// Unset settings.
|
||||
unset($CFG->recaptchapublickey);
|
||||
unset($CFG->recaptchaprivatekey);
|
||||
unset($CFG->enableloginrecaptcha);
|
||||
}
|
||||
|
||||
public function test_user_loggedin_event_exceptions() {
|
||||
|
||||
@@ -87,6 +87,9 @@ information provided here is intended especially for developers.
|
||||
to ensure in some test that the block drawer is closed. This helps with random failures due to the block drawer
|
||||
being forced open in all behat tests.
|
||||
* The core_useragent::get_device_type_list() function has been deprecated. Use core_useragent::devicetypes instead as a replacement.
|
||||
* New method login_captcha_enabled() is created to check whether the login captcha is enabled or not.
|
||||
* New method validate_login_captcha() is created to validate the login captcha.
|
||||
* A new parameter $loginrecaptcha has been added to the authenticate_user_login() to check whether the login captcha is needed to verify or not. The default value is false.
|
||||
|
||||
=== 4.2 ===
|
||||
|
||||
|
||||
+4
-1
@@ -151,7 +151,8 @@ if ($frm and isset($frm->username)) { // Login WITH
|
||||
} else {
|
||||
if (empty($errormsg)) {
|
||||
$logintoken = isset($frm->logintoken) ? $frm->logintoken : '';
|
||||
$user = authenticate_user_login($frm->username, $frm->password, false, $errorcode, $logintoken);
|
||||
$loginrecaptcha = $frm->{'g-recaptcha-response'} ?? false;
|
||||
$user = authenticate_user_login($frm->username, $frm->password, false, $errorcode, $logintoken, $loginrecaptcha);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +280,8 @@ if ($frm and isset($frm->username)) { // Login WITH
|
||||
if (empty($errormsg)) {
|
||||
if ($errorcode == AUTH_LOGIN_UNAUTHORISED) {
|
||||
$errormsg = get_string("unauthorisedlogin", "", $frm->username);
|
||||
} else if ($errorcode == AUTH_LOGIN_FAILED_RECAPTCHA) {
|
||||
$errormsg = get_string('missingrecaptchachallengefield');
|
||||
} else {
|
||||
$errormsg = get_string("invalidlogin");
|
||||
$errorcode = 3;
|
||||
|
||||
Reference in New Issue
Block a user