MDL-78534 Authentication: MFA Improve 2nd factor verify flow

Improve login page worklow.
This patch improves the display and UX when a user provides
their second factor during the login process. It includes
refactoring the display to use a template fed by renderer.
This commit is contained in:
Matt Porritt
2023-09-14 11:50:16 +10:00
parent 77a998006f
commit 9813e1eec6
12 changed files with 350 additions and 90 deletions
@@ -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 <peterburnett@catalyst-au.net>
* @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
@@ -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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
{"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 <http://www.gnu.org/licenses/>.\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 <[email protected]>\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"}
@@ -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;
}
};
+32 -25
View File
@@ -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();
@@ -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);
}
}
+108 -24
View File
@@ -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 . '&nbsp;' . $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);
}
}
+6 -8
View File
@@ -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.<br>This may be due to: <br> 1) Steps being locked - please wait a few minutes and try again.
<br> 2) Steps being failed - please double check the details in each step. <br> 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';
+36 -4
View File
@@ -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;
}
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%;
}
+3 -5
View File
@@ -23,8 +23,6 @@
{
}
}}
<div class='my-2'>
<h4>
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php'> {{#str}} guidance, tool_mfa {{/str}} </a>
</h4>
</div>
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php'>
{{#str}} guidance, tool_mfa {{/str}}<i class="icon fa fa-external-link fa-fw ml-1" aria-hidden="true"></i>
</a>
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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": "<form id=\"mfa-verify-form\" method=\"POST\" action=\"https:\/\/webserver\/admin\/tool\/mfa\/auth.php\"></form>",
"hasadditionalfactors": true,
"additionalfactors": [
{
"name": "webauthn",
"icon": "fa-hand-pointer",
"loginoption": "Use authenticator token"
}
],
"authurl": "https:\/\/webserver\/admin\/tool\/mfa\/auth.php",
"supportlink": "<a href=\"https:\/\/webserver\/user\/contactsitesupport.php\">Contact site support<\/a>",
"disablefactor": false
}
}}
<div>
<div class="row">
<div class="col-12">
<h2>{{#str}} verification, tool_mfa {{/str}}</h2>
<p>{{#str}} verification_desc, tool_mfa {{/str}}</p>
</div>
</div>
<div class="mb-4 border-bottom"></div>
<div class="d-flex align-items-start pb-2 {{#disablefactor}}text-muted{{/disablefactor}}">
<div class="pt-1" style="flex: 0 0 auto;">
<i class="fa {{factoricon}} fa-fw fa-2x" title="{{logintitle}}" role="img" aria-label="{{logintitle}}"></i>
</div>
<div style="flex: 1 1 auto; padding-left: 15px;" class="text-wrap">
<strong>{{logintitle}}</strong><br>
{{logindesc}}
</div>
</div>
</div>
<div class="mfa-verify-form">
{{{form}}}
</div>
{{#hasadditionalfactors}}
<div class="mb-6 mt-4 border-bottom"></div>
<p class="font-weight-bold mb-1">{{#str}} verifyalt, tool_mfa {{/str}}</p>
{{/hasadditionalfactors}}
{{#additionalfactors}}
<form id="{{uniqid}}-nextfactor" method="POST" action="{{{authurl}}}">
<input type="hidden" id="{{uniqid}}-factorname" name="factorname" value="{{name}}">
<button class="btn-link text-decoration-none tool-mfa-factor-choose d-flex align-items-center p-1 mb-2 {{#disable}}disabled{{/disable}}"
type="submit">
<span class="icon-no-margin icon-size-3 d-flex p-1 mr-2">
<i class="fa {{icon}} fa-fw fa-2x"
title="{{logintitle}}"
role="img"
aria-label="{{logintitle}}"></i>
</span>
<span class="tool-mfa-factor-choose-text mfa-login-option">{{loginoption}}</span>
</button>
</form>
{{/additionalfactors}}
<div class="form-container text-center mt-3">
<div class="d-flex justify-content-center">
<form id="cancelmfa" method="POST" action="{{{authurl}}}">
<input type="hidden" id="{{uniqid}}-logout" name="logout" value="true">
<input type="submit" class="btn btn-secondary"
name="cancelmfa" id="cancelmfa_button"
value="Cancel login" data-initial-value="Cancel login">
</form>
</div>
{{#supportlink}}
<div class="tool-mfa-site-support mt-4">
Need help? {{{supportlink}}}
</div>
{{/supportlink}}
</div>
@@ -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);
+2 -1
View File
@@ -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();