Merge branch 'MDL-53368-master-3' of https://github.com/HuongNV13/moodle

This commit is contained in:
Jun Pataleta 2023-08-24 22:51:26 +08:00
commit 29ec472284
No known key found for this signature in database
GPG Key ID: F83510526D99E2C7
11 changed files with 157 additions and 6 deletions

View File

@ -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);

View File

@ -73,6 +73,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.
@ -122,6 +124,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);
}
}
/**
@ -166,6 +174,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;
}

View File

@ -77,6 +77,15 @@ class block_login extends block_base {
$this->content->text .= ' class="form-control" value="" autocomplete="current-password"/>';
$this->content->text .= '</div>';
// ReCaptcha.
if (login_captcha_enabled()) {
require_once($CFG->libdir . '/recaptchalib_v2.php');
$this->content->text .= '<div class="form-group">';
$this->content->text .= recaptcha_get_challenge_html(RECAPTCHA_API_URL, $CFG->recaptchapublickey,
current_language(), true);
$this->content->text .= '</div>';
}
$this->content->text .= '<div class="form-group">';
$this->content->text .= '<input type="submit" class="btn btn-primary btn-block" value="'.get_string('login').'" />';
$this->content->text .= '</div>';

View File

@ -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.';

View File

@ -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.
*
@ -1043,6 +1046,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).
*

View File

@ -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, $SESSION;
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) {

View File

@ -104,9 +104,10 @@ function recaptcha_lang($lang = null) {
* @param string $apiurl URL for reCAPTCHA API
* @param string $pubkey The public key for reCAPTCHA
* @param string $lang Language to use. If not provided, get current language.
* @param bool $compactmode If true, use the compact widget.
* @return string - The HTML to be embedded in the user's form.
*/
function recaptcha_get_challenge_html($apiurl, $pubkey, $lang = null) {
function recaptcha_get_challenge_html($apiurl, $pubkey, $lang = null, bool $compactmode = false) {
global $CFG, $PAGE;
// To use reCAPTCHA you must have an API key.
@ -127,7 +128,10 @@ function recaptcha_get_challenge_html($apiurl, $pubkey, $lang = null) {
$apicode .= "</script>\n";
$return = html_writer::script($jscode, '');
$return .= html_writer::div('', 'recaptcha_element', array('id' => 'recaptcha_element'));
$return .= html_writer::div('', 'recaptcha_element', [
'id' => 'recaptcha_element',
'data-size' => ($compactmode ? 'compact' : 'normal'),
]);
$return .= $apicode;
return $return;
@ -182,7 +186,7 @@ function recaptcha_check_response($verifyurl, $privkey, $remoteip, $response) {
$checkresponse['error'] = '';
} else {
$checkresponse['isvalid'] = false;
$checkresponse['error'] = $curldata->{error-codes};
$checkresponse['error'] = $curldata->{'error-codes'};
}
} else {
$checkresponse['isvalid'] = false;

View File

@ -148,6 +148,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>

View File

@ -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() {

View File

@ -133,6 +133,11 @@ being forced open in all behat tests.
- \mod_quiz\task\legacy_quiz_accessrules_cron
- \mod_workshop\task\legacy_workshop_allocation_cron
Please, use the Task API instead: https://moodledev.io/docs/apis/subsystems/task
* 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.
* A new parameter $compactmode has been added to the recaptcha_get_challenge_html() to define whether the reCaptcha element should be displayed in the compact mode or not.
Default value is false.
=== 4.2 ===

View File

@ -152,7 +152,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);
}
}
@ -281,6 +282,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;