diff --git a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js index 70e0b8e87fd..a52bce52c29 100644 --- a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js +++ b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js @@ -1,10 +1,3 @@ -/** - * Module to autosubmit the verification code element when it reaches 6 characters. - * - * @module tool_mfa/autosubmit_verification_code - * @copyright 2020 Peter Burnett - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -define("tool_mfa/autosubmit_verification_code",[],(function(){return{init:function(){document.querySelector("#id_verificationcode").addEventListener("keyup",(function(){6==this.value.length&&this.closest("form").submit()}))}}})); +define("tool_mfa/autosubmit_verification_code",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{const codeInput=document.querySelector("#id_verificationcode"),codeForm=codeInput.closest("form"),submitButton=codeForm.querySelector("#id_submitbutton");codeInput.addEventListener("keyup",(function(){this.value.length>=6&&codeForm.submit()})),codeInput.disabled&&(submitButton.disabled=!0)}})); //# sourceMappingURL=autosubmit_verification_code.min.js.map \ No newline at end of file diff --git a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map index 28f202b5df6..6e69dcc6318 100644 --- a/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map +++ b/admin/tool/mfa/amd/build/autosubmit_verification_code.min.js.map @@ -1 +1 @@ -{"version":3,"file":"autosubmit_verification_code.min.js","sources":["../src/autosubmit_verification_code.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to autosubmit the verification code element when it reaches 6 characters.\n *\n * @module tool_mfa/autosubmit_verification_code\n * @copyright 2020 Peter Burnett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([], function() {\n return {\n init: function() {\n document.querySelector(\"#id_verificationcode\").addEventListener('keyup', function() {\n if (this.value.length == 6) {\n // Submits the closes form (parent).\n this.closest(\"form\").submit();\n }\n });\n }\n };\n});\n"],"names":["define","init","document","querySelector","addEventListener","this","value","length","closest","submit"],"mappings":";;;;;;;AAwBAA,+CAAO,IAAI,iBACA,CACHC,KAAM,WACFC,SAASC,cAAc,wBAAwBC,iBAAiB,SAAS,WAC5C,GAArBC,KAAKC,MAAMC,aAENC,QAAQ,QAAQC"} \ No newline at end of file +{"version":3,"file":"autosubmit_verification_code.min.js","sources":["../src/autosubmit_verification_code.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to autosubmit the verification code element when it reaches 6 characters.\n *\n * @module tool_mfa/autosubmit_verification_code\n * @copyright 2020 Peter Burnett \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const init = () => {\n const codeInput = document.querySelector(\"#id_verificationcode\");\n const codeForm = codeInput.closest(\"form\");\n const submitButton = codeForm.querySelector(\"#id_submitbutton\");\n\n // Event listener for code input field.\n codeInput.addEventListener('keyup', function() {\n if (this.value.length >= 6) {\n // Submits the closes form (parent).\n codeForm.submit();\n }\n });\n\n // Disable the submit button if the input field is disabled.\n // This occurs if there are no more attempts left for the factor.\n if (codeInput.disabled) {\n submitButton.disabled = true;\n }\n};\n"],"names":["codeInput","document","querySelector","codeForm","closest","submitButton","addEventListener","this","value","length","submit","disabled"],"mappings":"0KAwBoB,WACVA,UAAYC,SAASC,cAAc,wBACnCC,SAAWH,UAAUI,QAAQ,QAC7BC,aAAeF,SAASD,cAAc,oBAG5CF,UAAUM,iBAAiB,SAAS,WAC5BC,KAAKC,MAAMC,QAAU,GAErBN,SAASO,YAMbV,UAAUW,WACVN,aAAaM,UAAW"} \ No newline at end of file diff --git a/admin/tool/mfa/amd/src/autosubmit_verification_code.js b/admin/tool/mfa/amd/src/autosubmit_verification_code.js index 5d85bd17ac6..6b29304976c 100644 --- a/admin/tool/mfa/amd/src/autosubmit_verification_code.js +++ b/admin/tool/mfa/amd/src/autosubmit_verification_code.js @@ -22,15 +22,22 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define([], function() { - return { - init: function() { - document.querySelector("#id_verificationcode").addEventListener('keyup', function() { - if (this.value.length == 6) { - // Submits the closes form (parent). - this.closest("form").submit(); - } - }); +export const init = () => { + const codeInput = document.querySelector("#id_verificationcode"); + const codeForm = codeInput.closest("form"); + const submitButton = codeForm.querySelector("#id_submitbutton"); + + // Event listener for code input field. + codeInput.addEventListener('keyup', function() { + if (this.value.length >= 6) { + // Submits the closes form (parent). + codeForm.submit(); } - }; -}); + }); + + // Disable the submit button if the input field is disabled. + // This occurs if there are no more attempts left for the factor. + if (codeInput.disabled) { + submitButton.disabled = true; + } +}; diff --git a/admin/tool/mfa/auth.php b/admin/tool/mfa/auth.php index dcf302bdafc..52c64de1b62 100644 --- a/admin/tool/mfa/auth.php +++ b/admin/tool/mfa/auth.php @@ -27,18 +27,20 @@ require_once($CFG->dirroot . '/admin/tool/mfa/lib.php'); require_once($CFG->libdir.'/adminlib.php'); use tool_mfa\local\form\login_form; +use tool_mfa\manager; +use tool_mfa\plugininfo\factor; require_login(null, false); $context = context_user::instance($USER->id); $PAGE->set_context($context); $PAGE->set_url('/admin/tool/mfa/auth.php'); -$PAGE->set_pagelayout('secure'); +$PAGE->set_pagelayout('login'); $PAGE->blocks->show_only_fake_blocks(); $pagetitle = $SITE->shortname.': '.get_string('mfa', 'tool_mfa'); $PAGE->set_title($pagetitle); -// The only page action allowed here is a logout if it was requested. +// Logout if it was requested. $logout = optional_param('logout', false, PARAM_BOOL); if ($logout) { if (!empty($SESSION->wantsurl)) { @@ -49,62 +51,67 @@ if ($logout) { $wantsurl = new \moodle_url($CFG->wwwroot); } - \tool_mfa\manager::mfa_logout(); + manager::mfa_logout(); redirect($wantsurl); } $currenturl = new moodle_url('/admin/tool/mfa/auth.php'); // Perform state check. -\tool_mfa\manager::resolve_mfa_status(); +manager::resolve_mfa_status(); // We have a valid landing here, before doing any actions, clear any redir loop progress. -\tool_mfa\manager::clear_redirect_counter(); +manager::clear_redirect_counter(); + +// If a specific factor was requested, use it. +$pickedname = optional_param('factorname', false, PARAM_ALPHA); +$pickedfactor = factor::get_factor($pickedname); +$formfactor = optional_param('factor', false, PARAM_ALPHA); + +if ($pickedfactor && $pickedfactor->has_input() && $pickedfactor->get_state() == factor::STATE_UNKNOWN) { + $factor = $pickedfactor; +} else if ($formfactor) { + // Check if a factor was supplied by the form, such as for a form submission. + $factor = factor::get_factor($formfactor); +} else { + // Else, get the next factor that requires input. + $factor = factor::get_next_user_login_factor(); +} -$factor = \tool_mfa\plugininfo\factor::get_next_user_factor(); // If ok, perform form actions for input factor. -$form = new login_form($currenturl, ['factor' => $factor]); +$form = new login_form($currenturl, ['factor' => $factor], 'post', '', ['class' => 'ignoredirty']); if ($form->is_submitted()) { if (!$form->is_validated() && !$form->is_cancelled()) { // Increment the fail counter for the factor, // And let the factor handle locking logic. $factor->increment_lock_counter(); - \tool_mfa\manager::resolve_mfa_status(false); + manager::resolve_mfa_status(false); } else { // Set state from user actions. if ($form->is_cancelled()) { $factor->process_cancel_action(); // Move to next factor. - \tool_mfa\manager::resolve_mfa_status(true); + manager::resolve_mfa_status(true); } else { if ($data = $form->get_data()) { // Validation has passed, so before processing, lets action the global form submissions as well. $form->globalmanager->submit($data); // Did user submit something that causes a fail state? - if ($factor->get_state() == \tool_mfa\plugininfo\factor::STATE_FAIL) { - \tool_mfa\manager::resolve_mfa_status(true); + if ($factor->get_state() == factor::STATE_FAIL) { + manager::resolve_mfa_status(true); } - $factor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS); + $factor->set_state(factor::STATE_PASS); // Move to next factor. - \tool_mfa\manager::resolve_mfa_status(true); + manager::resolve_mfa_status(true); } } } } + $renderer = $PAGE->get_renderer('tool_mfa'); echo $OUTPUT->header(); - -\tool_mfa\manager::display_debug_notification(); - -echo $OUTPUT->heading(get_string('pluginname', 'factor_'.$factor->name)); -// Check if a notification is required for factor lockouts. -$remattempts = $factor->get_remaining_attempts(); -if ($remattempts < get_config('tool_mfa', 'lockout')) { - echo $OUTPUT->notification(get_string('lockoutnotification', 'tool_mfa', $remattempts), 'notifyerror'); -} -$form->display(); - -echo $renderer->guide_link(); +manager::display_debug_notification(); +echo $renderer->verification_form($factor, $form); echo $OUTPUT->footer(); diff --git a/admin/tool/mfa/classes/local/form/login_form.php b/admin/tool/mfa/classes/local/form/login_form.php index 62406cd5fad..6658db43b5a 100644 --- a/admin/tool/mfa/classes/local/form/login_form.php +++ b/admin/tool/mfa/classes/local/form/login_form.php @@ -69,6 +69,9 @@ class login_form extends \moodleform { $mform = $this->_form; $factor = $this->_customdata['factor']; $mform = $factor->login_form_definition($mform); + // Add a hidden field with the factor name so it is always available. + $factorname = $mform->addElement('hidden', 'factor', $factor->name); + $factorname->setType(PARAM_ALPHAEXT); $this->globalmanager->definition($mform); } @@ -86,7 +89,6 @@ class login_form extends \moodleform { $buttonarray = []; $buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('loginsubmit', 'factor_' . $factor->name)); - $buttonarray[] = &$mform->createElement('cancel', '', get_string('loginskip', 'factor_' . $factor->name)); $mform->addGroup($buttonarray, 'buttonar', '', [' '], false); $mform->closeHeaderBefore('buttonar'); } @@ -110,4 +112,45 @@ class login_form extends \moodleform { return $errors; } + + /** + * Returns error corresponding to validated element. + * + * @param string $elementname Name of form element to check. + * @return string|null Error message corresponding to the validated element. + */ + public function get_element_error(string $elementname): ?string { + return $this->_form->getElementError($elementname); + } + + /** + * Set an error message for a form element. + * + * @param string $elementname Name of form element to set error for. + * @param string $error Error message, if empty then removes the current error message. + * @return void + */ + public function set_element_error(string $elementname, string $error): void { + $this->_form->setElementError($elementname, $error); + } + + /** + * Freeze a form element. + * + * @param string $elementname Name of form element to freeze. + * @return void + */ + public function freeze(string $elementname): void { + $this->_form->freeze($elementname); + } + + /** + * Returns true if the form element exists. + * + * @param string $elementname Name of form element to check. + * @return bool + */ + public function element_exists(string $elementname): bool { + return $this->_form->elementExists($elementname); + } } diff --git a/admin/tool/mfa/classes/output/renderer.php b/admin/tool/mfa/classes/output/renderer.php index e32ad7069f7..a2b35cfd0fe 100644 --- a/admin/tool/mfa/classes/output/renderer.php +++ b/admin/tool/mfa/classes/output/renderer.php @@ -234,14 +234,14 @@ class renderer extends \plugin_renderer_base { $linktext = \html_writer::link($supportpage, $supportpage); $notification .= $linktext; } - $return = $this->output->notification($notification, 'notifyerror'); + $return = $this->output->notification($notification, 'notifyerror', false); // Logout button. $url = new \moodle_url('/admin/tool/mfa/auth.php', ['logout' => 1]); - $btn = new \single_button($url, get_string('logout'), 'post', true); + $btn = new \single_button($url, get_string('logout'), 'post', \single_button::BUTTON_PRIMARY); $return .= $this->render($btn); - $return .= $this->guide_link(); + $return .= $this->get_support_link(); return $return; } @@ -421,9 +421,9 @@ class renderer extends \plugin_renderer_base { $lockedusers = $DB->count_records_sql($sql, [$factor->name, $locklevel]); $enabled = $factor->is_enabled() ? \html_writer::tag('b', get_string('yes')) : get_string('no'); - $actions = \html_writer::link( new moodle_url($this->page->url, + $actions = \html_writer::link( new \moodle_url($this->page->url, ['reset' => $factor->name, 'sesskey' => sesskey()]), get_string('performbulk', 'tool_mfa')); - $lockedusers = \html_writer::link(new moodle_url($this->page->url, ['view' => $factor->name]), $lockedusers); + $lockedusers = \html_writer::link(new \moodle_url($this->page->url, ['view' => $factor->name]), $lockedusers); $table->data[] = [ $factor->get_display_name(), @@ -439,13 +439,13 @@ class renderer extends \plugin_renderer_base { /** * Displays a table of all users with a locked instance of the given factor. * - * @param object $factor the factor class + * @param object_factor $factor the factor class * @return string the HTML for the table */ - public function factor_locked_users_table(object $factor): string { + public function factor_locked_users_table(object_factor $factor): string { global $DB; - $table = new html_table(); + $table = new \html_table(); $table->attributes['class'] = 'generaltable table table-bordered w-auto'; $table->attributes['style'] = 'width: auto; min-width: 50%'; $table->head = [ @@ -477,25 +477,25 @@ class renderer extends \plugin_renderer_base { foreach ($records as $record) { // Construct profile link. - $proflink = \html_writer::link(new moodle_url('/user/profile.php', + $proflink = \html_writer::link(new \moodle_url('/user/profile.php', ['id' => $record->id]), fullname($record)); // IP link. - $creatediplink = \html_writer::link(new moodle_url('/iplookup/index.php', + $creatediplink = \html_writer::link(new \moodle_url('/iplookup/index.php', ['ip' => $record->createdfromip]), $record->createdfromip); - $lastiplink = \html_writer::link(new moodle_url('/iplookup/index.php', + $lastiplink = \html_writer::link(new \moodle_url('/iplookup/index.php', ['ip' => $record->lastip]), $record->lastip); // Deep link to logs. $logicon = $this->pix_icon('i/report', get_string('userlogs', 'tool_mfa')); - $actions = \html_writer::link(new moodle_url('/report/log/index.php', [ + $actions = \html_writer::link(new \moodle_url('/report/log/index.php', [ 'id' => 1, // Site. 'user' => $record->id, ]), $logicon); - $action = new confirm_action(get_string('resetfactorconfirm', 'tool_mfa', fullname($record))); + $action = new \confirm_action(get_string('resetfactorconfirm', 'tool_mfa', fullname($record))); $actions .= $this->action_link( - new moodle_url($this->page->url, ['reset' => $factor->name, 'id' => $record->id, 'sesskey' => sesskey()]), + new \moodle_url($this->page->url, ['reset' => $factor->name, 'id' => $record->id, 'sesskey' => sesskey()]), $this->pix_icon('t/delete', get_string('resetconfirm', 'tool_mfa')), $action ); @@ -514,17 +514,20 @@ class renderer extends \plugin_renderer_base { } /** - * Returns a html section render of the guide link template + * Returns a rendered support link. + * If the MFA guidance page is enabled, this is returned. + * Otherwise, the site support link is returned. + * If neither support link is configured, an empty string is returned. * * @return string */ - public function guide_link(): string { - if (!get_config('tool_mfa', 'guidance')) { - return ''; + public function get_support_link(): string { + // Try the guidance page link first. + if (get_config('tool_mfa', 'guidance')) { + return $this->render_from_template('tool_mfa/guide_link', []); + } else { + return $this->output->supportemail([], true); } - $html = $this->heading(get_string('needhelp', 'tool_mfa'), 3); - $html .= $this->render_from_template('tool_mfa/guide_link', []); - return $this->notification($html, 'info'); } /** @@ -532,17 +535,17 @@ class renderer extends \plugin_renderer_base { * * In certain situations, includes a script element which adds autosubmission behaviour. * - * @param \HTML_QuickForm_element $element element + * @param mixed $element element * @param bool $required if input is required field * @param bool $advanced if input is an advanced field * @param string|null $error error message to display * @param bool $ingroup True if this element is rendered as part of a group * @return mixed string|bool */ - public function mform_element(HTML_QuickForm_element $element, bool $required, + public function mform_element(mixed $element, bool $required, bool $advanced, string|null $error, bool $ingroup): string|bool { $script = null; - if ($element instanceof tool_mfa\local\form\verification_field) { + if ($element instanceof \tool_mfa\local\form\verification_field) { if ($this->page->pagelayout === 'secure') { $script = $element->secure_js(); } @@ -556,4 +559,85 @@ class renderer extends \plugin_renderer_base { return $result; } + + /** + * Renders the verification form. + * + * @param object_factor $factor The factor to render the form for. + * @param login_form $form The login form object. + * @return string + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function verification_form(object_factor $factor, login_form $form): string { + $allloginfactors = factor::get_all_user_login_factors(); + $additionalfactors = []; + $disabledfactors = []; + $displaycount = 0; + $disablefactor = false; + + foreach ($allloginfactors as $loginfactor) { + if ($loginfactor->name != $factor->name) { + $additionalfactor = [ + 'name' => $loginfactor->name, + 'icon' => $loginfactor->get_icon(), + 'loginoption' => get_string('loginoption', 'factor_' . $loginfactor->name), + ]; + // We mark the factor as disabled if it is locked. + // We store the disabled factors in a separate array so that they can be displayed at the bottom of the template. + if ($loginfactor->get_state() == factor::STATE_LOCKED) { + $additionalfactor['loginoption'] = get_string('locked', 'tool_mfa', $additionalfactor['loginoption']); + $additionalfactor['disable'] = true; + $disabledfactors[] = $additionalfactor; + } else { + $additionalfactors[] = $additionalfactor; + } + $displaycount++; + } + } + + // We merge the additional factors placing the disabled ones last. + $alladitionalfactors = array_merge($additionalfactors, $disabledfactors); + $hasadditionalfactors = $displaycount > 0; + $authurl = new \moodle_url('/admin/tool/mfa/auth.php'); + + // Set the form to better display vertically. + $form->set_display_vertical(); + + // Check if we need to display a remaining attempts message. + $remattempts = $factor->get_remaining_attempts(); + $verificationerror = $form->get_element_error('verificationcode'); + if ($remattempts < get_config('tool_mfa', 'lockout') && !empty($verificationerror)) { + // Update the validation error for the code form field to include the remaining attempts. + $remattemptsstr = get_string('lockoutnotification', 'tool_mfa', $factor->get_remaining_attempts()); + $updatederror = $verificationerror . ' ' . $remattemptsstr; + $form->set_element_error('verificationcode', $updatederror); + } + + // If all attempts for this factor have been used, disable the form. + // This forces the user to choose another factor or cancel their login. + if ($remattempts <= 0) { + $disablefactor = true; + $form->freeze('verificationcode'); + + // Handle the trust factor if present. + if ($form->element_exists('factor_token_trust')) { + $form->freeze('factor_token_trust'); + } + } + + $context = [ + 'logintitle' => get_string('logintitle', 'factor_'.$factor->name), + 'logindesc' => $factor->get_login_desc(), + 'factoricon' => $factor->get_icon(), + 'form' => $form->render(), + 'hasadditionalfactors' => $hasadditionalfactors, + 'additionalfactors' => $alladitionalfactors, + 'authurl' => $authurl->out(), + 'supportlink' => $this->get_support_link(), + 'disablefactor' => $disablefactor + ]; + return $this->render_from_template('tool_mfa/verification_form', $context); + } } diff --git a/admin/tool/mfa/lang/en/tool_mfa.php b/admin/tool/mfa/lang/en/tool_mfa.php index 3ce90a27f3d..1e4f0e1a45b 100644 --- a/admin/tool/mfa/lang/en/tool_mfa.php +++ b/admin/tool/mfa/lang/en/tool_mfa.php @@ -32,7 +32,6 @@ $string['combination'] = 'Combination'; $string['connector'] = 'AND'; $string['created'] = 'Created'; $string['createdfromip'] = 'Created from IP'; -$string['debugmode:currentweight'] = 'Current weight: {$a}'; $string['debugmode:heading'] = 'Debug mode'; $string['devicename'] = 'Device'; $string['email:subject'] = 'Unable to login to {$a}'; @@ -41,7 +40,6 @@ $string['error:actionnotfound'] = 'Action \'{$a}\' not supported'; $string['error:directaccess'] = 'This page shouldn\'t be accessed directly'; $string['error:factornotenabled'] = 'MFA Factor \'{$a}\' not enabled'; $string['error:factornotfound'] = 'MFA Factor \'{$a}\' not found'; -$string['error:home'] = 'Click here to return home.'; $string['error:notenoughfactors'] = 'Unable to authenticate'; $string['error:reauth'] = 'We couldn\'t confirm your identity sufficiently to meet this sites authentication security policy.
This may be due to:
1) Steps being locked - please wait a few minutes and try again.
2) Steps being failed - please double check the details in each step.
3) Steps were skipped - please reload this page or try logging in again.'; @@ -59,7 +57,6 @@ $string['event:userpassedmfa'] = 'Verification passed'; $string['event:userrevokedfactor'] = 'Factor revocation'; $string['event:usersetupfactor'] = 'Factor setup'; $string['factor'] = 'Factor'; -$string['factorlocked'] = 'Factor \'{$a}\' has been locked due to exceeded failed attempts.'; $string['factorreport'] = 'All factor report'; $string['factorreset'] = 'Your MFA \'{$a->factor}\' has been reset by a site administrator. You may need to set up this factor again. {$a->url}'; $string['factorresetall'] = 'All your MFA factors have been reset by a site administrator. You may need to set up these factors again. {$a}'; @@ -67,15 +64,14 @@ $string['factorrevoked'] = 'Factor \'{$a}\' successfully revoked.'; $string['factorsetup'] = 'Factor \'{$a}\' successfully setup.'; $string['fallback'] = 'Fallback factor'; $string['fallback_info'] = 'This factor is a fallback if no other factors are configured. This factor will always fail.'; -$string['gotourl'] = 'Go to your original URL: '; $string['guidance'] = 'MFA user guide'; $string['inputrequired'] = 'User input'; $string['ipatcreation'] = 'IP address when factor created'; $string['lastverified'] = 'Last verified'; +$string['locked'] = '{$a} (Unavailable)'; $string['lockedusersforallfactors'] = 'Locked users: All factors'; $string['lockedusersforfactor'] = 'Locked users: {$a}'; -$string['lockoutnotification'] = 'You have {$a} verification attempts remaining for this factor.'; -$string['lookbackperiod'] = 'Showing MFA information from {$a} onwards.'; +$string['lockoutnotification'] = 'You have {$a} attempts left.'; $string['mfa'] = 'MFA'; $string['mfa:mfaaccess'] = 'Interact with MFA'; $string['mfareports'] = 'MFA reports'; @@ -161,7 +157,9 @@ $string['userempty'] = 'User cannot be empty.'; $string['userlogs'] = 'User logs'; $string['usernotfound'] = 'Unable to locate user.'; $string['usersauthedinperiod'] = 'Logged in'; -$string['verificationcode'] = 'Enter verification code for confirmation'; +$string['verification'] = '2-Step Verification'; +$string['verification_desc'] = 'To keep your account safe, we need to check that this is really you.'; +$string['verificationcode'] = 'Enter code'; $string['verificationcode_help'] = 'The verification code provided by the current authentication factor.'; -$string['viewlockedusers'] = 'View locked users'; +$string['verifyalt'] = 'Try another way to verify:'; $string['weight'] = 'Weight'; diff --git a/admin/tool/mfa/styles.css b/admin/tool/mfa/styles.css index 4d7301fc60b..2a376f12b12 100644 --- a/admin/tool/mfa/styles.css +++ b/admin/tool/mfa/styles.css @@ -1,8 +1,40 @@ input.tool-mfa-verification-code, .tool-mfa-verification-code input { /* Some elements must be important to override form element*/ - font-size: 1.25em; - letter-spacing: 1.05em; + font-size: 1.5em !important; /* stylelint-disable-line declaration-no-important */ font-family: monospace; - width: 11.5em; -} \ No newline at end of file + text-align: center; + letter-spacing: 1.0em; + font-weight: bold; +} + +.tool-mfa-factor-choose, +.tool-mfa-factor-choose:hover { + background-color: #fff; + border-color: #3584c9; + border-width: 1px; + width: 100%; + border-radius: 0.5rem; +} + +.tool-mfa-factor-choose:hover, +.tool-mfa-factor-choose:hover .tool-mfa-factor-choose-text { + text-decoration: underline; + color: #094478; + background-color: #cfe2f2; +} + +.mfa-verify-form input[type="submit"] { + height: 50px; + font-size: 1.05rem; +} + +.mfa-verify-form .invalid-feedback { + font-weight: 700; +} + +.mfa-verify-form .form-group.fitem, +.mfa-verify-form .form-group.fitem > span, +.mfa-verify-form .form-group.fitem > span > input { + width: 100%; +} diff --git a/admin/tool/mfa/templates/guide_link.mustache b/admin/tool/mfa/templates/guide_link.mustache index 313c99cf92b..dfdef6237ca 100644 --- a/admin/tool/mfa/templates/guide_link.mustache +++ b/admin/tool/mfa/templates/guide_link.mustache @@ -23,8 +23,6 @@ { } }} - \ No newline at end of file + + {{#str}} guidance, tool_mfa {{/str}} + diff --git a/admin/tool/mfa/templates/verification_form.mustache b/admin/tool/mfa/templates/verification_form.mustache new file mode 100644 index 00000000000..1de5d3b8f52 --- /dev/null +++ b/admin/tool/mfa/templates/verification_form.mustache @@ -0,0 +1,97 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_mfa/verification_form + + Template to display the MFA verification form and options. + + Example context (json): + { + "logintitle": "We've just sent a 6-digit code to your email", + "logindesc": "Use the Google Authenticator app to generate a code.", + "factoricon": "fa-envelope", + "form": "
", + "hasadditionalfactors": true, + "additionalfactors": [ + { + "name": "webauthn", + "icon": "fa-hand-pointer", + "loginoption": "Use authenticator token" + } + ], + "authurl": "https:\/\/webserver\/admin\/tool\/mfa\/auth.php", + "supportlink": "Contact site support<\/a>", + "disablefactor": false + } +}} + +
+
+
+

{{#str}} verification, tool_mfa {{/str}}

+

{{#str}} verification_desc, tool_mfa {{/str}}

+
+
+
+
+
+ +
+
+ {{logintitle}}
+ {{logindesc}} +
+
+
+
+ {{{form}}} +
+{{#hasadditionalfactors}} +
+

{{#str}} verifyalt, tool_mfa {{/str}}

+{{/hasadditionalfactors}} +{{#additionalfactors}} +
+ + +
+{{/additionalfactors}} +
+
+
+ + +
+
+ {{#supportlink}} +
+ Need help? {{{supportlink}}} +
+ {{/supportlink}} +
+ diff --git a/admin/tool/mfa/tests/plugininfo_factor_test.php b/admin/tool/mfa/tests/plugininfo_factor_test.php index 10e24332ed9..c1c21a6d33b 100644 --- a/admin/tool/mfa/tests/plugininfo_factor_test.php +++ b/admin/tool/mfa/tests/plugininfo_factor_test.php @@ -29,14 +29,14 @@ class plugininfo_factor_test extends \advanced_testcase { /** * Tests getting next user factor * - * @covers ::get_next_user_factor + * @covers ::get_next_user_login_factor * @covers ::setup_user_factor * @covers ::get_enabled_factors * @covers ::is_enabled * @covers ::has_setup * @covers ::get_active_user_factor_types */ - public function test_get_next_user_factor() { + public function test_get_next_user_login_factor() { $this->resetAfterTest(true); diff --git a/admin/tool/mfa/user_preferences.php b/admin/tool/mfa/user_preferences.php index 94f7579fd1e..8dac427b152 100644 --- a/admin/tool/mfa/user_preferences.php +++ b/admin/tool/mfa/user_preferences.php @@ -61,7 +61,8 @@ if (!empty($action)) { echo $OUTPUT->active_factors(); echo $OUTPUT->available_factors(); -echo $OUTPUT->guide_link(); +$renderer = $PAGE->get_renderer('tool_mfa'); +echo $renderer->get_support_link(); \tool_mfa\manager::display_debug_notification();