MDL-79920 factor_totp: Improvements to the totp setup

This commit is contained in:
David Woloszyn 2024-03-04 17:01:12 +11:00
parent 71a5622c71
commit dbef09ab19
5 changed files with 114 additions and 45 deletions

View File

@ -39,8 +39,9 @@ class verification_field extends \MoodleQuickForm_text {
*
* @param array $attributes
* @param boolean $auth is this constructed in auth.php loginform_* definitions. Set to false to prevent autosubmission of form.
* @param string|null $elementlabel Provide a different element label.
*/
public function __construct($attributes = null, $auth = true) {
public function __construct($attributes = null, $auth = true, string $elementlabel = null) {
global $PAGE;
// Force attributes.
@ -51,7 +52,8 @@ class verification_field extends \MoodleQuickForm_text {
$attributes['autocomplete'] = 'one-time-code';
$attributes['inputmode'] = 'numeric';
$attributes['pattern'] = '[0-9]*';
$attributes['class'] = 'tool-mfa-verification-code font-weight-bold';
// Overwrite default classes if set.
$attributes['class'] = isset($attributes['class']) ? $attributes['class'] : 'tool-mfa-verification-code font-weight-bold';
$attributes['maxlength'] = 6;
// If we aren't on the auth page, this might be part of a larger form such as for setup.
@ -68,7 +70,8 @@ class verification_field extends \MoodleQuickForm_text {
// Force element name to match JS.
$elementname = 'verificationcode';
$elementlabel = get_string('verificationcode', 'tool_mfa');
// Overwrite default element label if set.
$elementlabel = !empty($elementlabel) ? $elementlabel : get_string('entercode', 'tool_mfa');
return parent::__construct($elementname, $elementlabel, $attributes);
}

View File

@ -95,8 +95,7 @@ class factor extends object_factor_base {
$uri = $this->generate_totp_uri($secret);
$qrcode = new \TCPDF2DBarcode($uri, 'QRCODE');
$image = $qrcode->getBarcodePngData(7, 7);
$html = \html_writer::tag('p', get_string('setupfactor:scanwithapp', 'factor_totp'));
$html .= \html_writer::img('data:image/png;base64,' . base64_encode($image), '', ['width' => '150px']);
$html = \html_writer::img('data:image/png;base64,' . base64_encode($image), '', ['width' => '150px']);
return $html;
}
@ -143,33 +142,36 @@ class factor extends object_factor_base {
// Array of elements to allow XSS.
$xssallowedelements = [];
$mform->addElement('html', $OUTPUT->heading(get_string('setupfactor', 'factor_totp'), 2));
$mform->addElement('html', \html_writer::tag('p', get_string('info', 'factor_totp')));
$mform->addElement('html', \html_writer::tag('hr', ''));
$headingstring = $mform->elementExists('replaceid') ? 'replacefactor' : 'setupfactor';
$mform->addElement('html', $OUTPUT->heading(get_string($headingstring, 'factor_totp'), 2));
$mform->addElement('text', 'devicename', get_string('devicename', 'factor_totp'), [
$html = \html_writer::tag('p', get_string('setupfactor:intro', 'factor_totp'));
$mform->addElement('html', $html);
// Device name.
$html = \html_writer::tag('p', get_string('setupfactor:instructionsdevicename', 'factor_totp'), ['class' => 'bold']);
$mform->addElement('html', $html);
$mform->addElement('text', 'devicename', get_string('setupfactor:devicename', 'factor_totp'), [
'placeholder' => get_string('devicenameexample', 'factor_totp'),
'autofocus' => 'autofocus',
]);
$mform->addHelpButton('devicename', 'devicename', 'factor_totp');
$mform->setType('devicename', PARAM_TEXT);
$mform->addRule('devicename', get_string('required'), 'required', null, 'client');
// Scan.
$html = \html_writer::tag('p', get_string('setupfactor:devicenameinfo', 'factor_totp'));
$mform->addElement('static', 'devicenameinfo', '', $html);
// Scan QR code.
$html = \html_writer::tag('p', get_string('setupfactor:instructionsscan', 'factor_totp'), ['class' => 'bold']);
$mform->addElement('html', $html);
$secretfield = $mform->getElement('secret');
$secret = $secretfield->getValue();
$qrcode = $this->generate_qrcode($secret);
$html = \html_writer::tag('p', $qrcode);
$xssallowedelements[] = $mform->addElement('static', 'scan', get_string('setupfactor:scan', 'factor_totp'), $html);
// Link.
if (get_config('factor_totp', 'totplink')) {
$uri = $this->generate_totp_uri($secret);
$html = $OUTPUT->action_link($uri, get_string('setupfactor:linklabel', 'factor_totp'));
$xssallowedelements[] = $mform->addElement('static', 'link', get_string('setupfactor:link', 'factor_totp'), $html);
$mform->addHelpButton('link', 'setupfactor:link', 'factor_totp');
}
$mform->addElement('static', 'scan', '', $html);
// Enter manually.
$secret = wordwrap($secret, 4, ' ', true) . '</code>';
@ -186,19 +188,15 @@ class factor extends object_factor_base {
];
$html = \html_writer::table($manualtable);
$html = \html_writer::tag('p', get_string('setupfactor:enter', 'factor_totp')) . $html;
// Wrap the table in a couple of divs to be controlled via bootstrap.
$html = \html_writer::div($html, 'card card-body', ['style' => 'padding-left: 0 !important;']);
$html = \html_writer::div($html, 'collapse', ['id' => 'collapseManualAttributes']);
$togglelink = \html_writer::tag('btn', get_string('setupfactor:scanfail', 'factor_totp'), [
'class' => 'btn btn-secondary',
'type' => 'button',
$togglelink = \html_writer::tag('a', get_string('setupfactor:link', 'factor_totp'), [
'data-toggle' => 'collapse',
'data-target' => '#collapseManualAttributes',
'aria-expanded' => 'false',
'aria-controls' => 'collapseManualAttributes',
'style' => 'font-size: 14px;',
'href' => '#',
]);
$html = $togglelink . $html;
@ -211,9 +209,17 @@ class factor extends object_factor_base {
}
}
$mform->addElement(new \tool_mfa\local\form\verification_field(null, false));
// Verification.
$html = \html_writer::tag('p', get_string('setupfactor:instructionsverification', 'factor_totp'), ['class' => 'bold']);
$mform->addElement('html', $html);
$verificationfield = new \tool_mfa\local\form\verification_field(
attributes: ['class' => 'tool-mfa-verification-code'],
auth: false,
elementlabel: get_string('setupfactor:verificationcode', 'factor_totp'),
);
$mform->addElement($verificationfield);
$mform->setType('verificationcode', PARAM_ALPHANUM);
$mform->addHelpButton('verificationcode', 'verificationcode', 'factor_totp');
$mform->addRule('verificationcode', get_string('required'), 'required', null, 'client');
return $mform;
@ -366,7 +372,7 @@ class factor extends object_factor_base {
], '*', IGNORE_MULTIPLE);
if ($record) {
\core\notification::warning(get_string('error:alreadyregistered', 'factor_totp'));
return $record;
return null;
}
$id = $DB->insert_record('tool_mfa', $row);
@ -379,6 +385,35 @@ class factor extends object_factor_base {
return null;
}
/**
* TOTP Factor implementation with replacement of existing factor.
*
* @param stdClass $data The new factor data.
* @param int $id The id of the factor to replace.
* @return stdClass|null the factor record, or null.
*/
public function replace_user_factor(stdClass $data, int $id): stdClass|null {
global $DB, $USER;
$oldrecord = $DB->get_record('tool_mfa', ['id' => $id]);
$newrecord = null;
// Ensure we have a valid existing record before setting the new one.
if ($oldrecord) {
$newrecord = $this->setup_user_factor($data);
}
// Ensure the new record was created before revoking the old.
if ($newrecord) {
$this->revoke_user_factor($id);
} else {
\core\notification::warning(get_string('error:couldnotreplace', 'tool_mfa'));
return null;
}
$this->create_event_after_factor_setup($USER);
return $newrecord ?? null;
}
/**
* TOTP Factor implementation.
*
@ -399,6 +434,13 @@ class factor extends object_factor_base {
return true;
}
/**
* TOTP Factor implementation.
*/
public function has_replace(): bool {
return true;
}
/**
* TOTP Factor implementation.
*
@ -447,6 +489,15 @@ class factor extends object_factor_base {
* {@inheritDoc}
*/
public function get_setup_string(): string {
return get_string('factorsetup', 'factor_totp');
return get_string('setupfactorbutton', 'factor_totp');
}
/**
* Gets the string for manage button on preferences page.
*
* @return string
*/
public function get_manage_string(): string {
return get_string('managefactorbutton', 'factor_totp');
}
}

View File

@ -0,0 +1,2 @@
setupfactor:scanfail,factor_totp
setupfactor:scan,factor_totp

View File

@ -24,7 +24,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['action:revoke'] = 'Revoke time-based one-time password (TOTP) authenticator';
$string['action:manage'] = 'Manage time-based one-time password (TOTP) authenticator';
$string['action:revoke'] = 'Remove time-based one-time password (TOTP) authenticator';
$string['devicename'] = 'Device label';
$string['devicename_help'] = 'This is the device you have an authenticator app installed on. You can set up multiple devices so this label helps track which ones are being used. You should set up each device with their own unique code so they can be revoked separately.';
$string['devicenameexample'] = 'eg "Work iPhone 11"';
@ -36,36 +37,48 @@ $string['error:oldcode'] = 'This code is too old. Please verify the time on your
Current system time is {$a}.';
$string['error:wrongverification'] = 'Incorrect verification code.';
$string['factorsetup'] = 'App setup';
$string['info'] = '<p>Use any time-based one-time password (TOTP) authenticator app on your device to generate a verification code, even when it is offline.</p>
<p>For example <a href="https://2fas.com/">2FAS Auth</a>, <a href="https://freeotp.github.io/">FreeOTP</a>, Google Authenticator, Microsoft Authenticator or Twilio Authy.</p>
<p>Note: Please ensure your device time and date has been set to "Auto" or "Network provided".</p>';
$string['info'] = 'Generate a verification code using an authenticator app.';
$string['logindesc'] = 'Use the authenticator app in your mobile device to generate a code.';
$string['loginoption'] = 'Use Authenticator application';
$string['loginskip'] = 'I don\'t have my device';
$string['loginsubmit'] = 'Continue';
$string['logintitle'] = 'Verify it\'s you by mobile app';
$string['managefactor'] = 'Manage authenticator app';
$string['managefactorbutton'] = 'Manage';
$string['manageinfo'] = 'You are using \'{$a}\' to authenticate.';
$string['pluginname'] = 'Authenticator app';
$string['privacy:metadata'] = 'The Authenticator app factor plugin does not store any personal data.';
$string['replacefactor'] = 'Replace authenticator app';
$string['replacefactorconfirmation'] = 'Replace \'{$a}\' authenticator app?';
$string['revokefactorconfirmation'] = 'Remove \'{$a}\' authenticator app?';
$string['settings:totplink'] = 'Show mobile app setup link';
$string['settings:totplink_help'] = 'If enabled the user will see a 3rd setup option with a direct otpauth:// link';
$string['settings:window'] = 'TOTP verification window';
$string['settings:window_help'] = 'How long each code is valid for. You can set this to a higher value as a workaround if your users device clocks are often slightly wrong.
Rounded down to the nearest 30 seconds, which is the time between new generated codes.';
$string['setupfactor'] = 'TOTP authenticator setup';
Rounded down to the nearest 30 seconds, which is the time between new generated codes.';
$string['setupfactor'] = 'Set up authenticator app';
$string['setupfactorbutton'] = 'Set up';
$string['setupfactor:account'] = 'Account:';
$string['setupfactor:enter'] = 'Enter details manually:';
$string['setupfactor:devicename'] = 'Device name';
$string['setupfactor:devicenameinfo'] = 'This helps you identify which device receives the verification code.';
$string['setupfactor:enter'] = 'Enter details manually';
$string['setupfactor:instructionsdevicename'] = '1. Give your device a name.';
$string['setupfactor:instructionsscan'] = '2. Scan the QR code with your authenticator app.';
$string['setupfactor:instructionsverification'] = '3. Enter the verification code.';
$string['setupfactor:intro'] = 'To set up this method, you need to have a device with an authenticator app. If you don\'t have an app, you can download one. For example, <a href="https://2fas.com/" target="_blank">2FAS Auth</a>, <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a>, Google Authenticator, Microsoft Authenticator or Twilio Authy.';
$string['setupfactor:key'] = 'Secret key: ';
$string['setupfactor:link'] = '<b> OR </b> open mobile app:';
$string['setupfactor:link'] = 'Or enter details manually.';
$string['setupfactor:link_help'] = 'If you are on a mobile device and already have an authenticator app installed this link may work. Note that using TOTP on the same device as you login on can weaken the benefits of MFA.';
$string['setupfactor:linklabel'] = 'Open app already installed on this device';
$string['setupfactor:mode'] = 'Mode:';
$string['setupfactor:mode:timebased'] = 'Time-based';
$string['setupfactor:scan'] = 'Scan QR code:';
$string['setupfactor:scanfail'] = 'Can\'t scan?';
$string['setupfactor:scanwithapp'] = 'Scan QR code with your chosen authenticator application.';
$string['setupfactor:verificationcode'] = 'Verification code';
$string['summarycondition'] = 'using a TOTP app';
$string['systimeformat'] = '%l:%M:%S %P %Z';
$string['verificationcode'] = 'Enter your 6 digit verification code';
$string['verificationcode_help'] = 'Open your authenticator app such as Google Authenticator and look for the 6 digit code which matches this site and username';
// Deprecated since Moodle 4.4.
$string['setupfactor:scanfail'] = 'Can\'t scan?';
$string['setupfactor:scan'] = 'Scan QR code';

View File

@ -135,11 +135,11 @@ class factor_test extends \advanced_testcase {
'secret' => 'fakekey',
'devicename' => 'fakedevice',
];
$record = $totpfactor->setup_user_factor((object) $totpdata);
$totpfactor->setup_user_factor((object) $totpdata);
// Trying to add the same TOTP should return the existing record (exactly).
// Trying to add the same TOTP should return null.
$anotherecord = $totpfactor->setup_user_factor((object) $totpdata);
$this->assertEquals($record, $anotherecord);
$this->assertNull($anotherecord);
// The total count for factors added should be 1 at this point.
$count = $DB->count_records('tool_mfa');