first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,271 @@
<?php
// 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/>.
namespace tool_mfa\local;
use tool_mfa\local\factor\object_factor_base;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/ddllib.php');
require_once($CFG->libdir.'/xmlize.php');
require_once($CFG->libdir.'/messagelib.php');
/**
* Admin setting for MFA.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_managemfa extends \admin_setting {
/**
* Calls parent::__construct with specific arguments
*/
public function __construct() {
$this->nosave = true;
parent::__construct('mfaui', get_string('mfasettings', 'tool_mfa'), '', '');
}
/**
* Always returns true
*
* @return bool
*/
public function get_setting(): bool {
return true;
}
/**
* Always returns '' and doesn't write anything
*
* @param mixed $data
* @return string Always returns ''
*/
public function write_setting($data): string {
return '';
}
/**
* Returns XHTML to display Manage MFA admin page.
*
* @param mixed $data Unused
* @param string $query
*
* @return string highlight
* @throws \coding_exception
* @throws \moodle_exception
*/
public function output_html($data, $query=''): string {
global $OUTPUT;
$return = $OUTPUT->box_start('generalbox');
$return .= $this->define_manage_mfa_table();
$return .= $OUTPUT->box_end();
$return .= $OUTPUT->heading(get_string('settings:combinations', 'tool_mfa'), 3);
$return .= $OUTPUT->box_start('generalbox');
$return .= $this->define_factor_combinations_table();
$return .= $OUTPUT->box_end();
return highlight($query, $return);
}
/**
* Defines main table with configurable factors.
*
* @return string HTML code
* @throws \coding_exception
* @throws \moodle_exception
*/
public function define_manage_mfa_table() {
global $OUTPUT;
$sesskey = sesskey();
$txt = get_strings(['enable', 'disable', 'moveup', 'movedown', 'order', 'settings']);
$txt->factor = get_string('factor', 'tool_mfa');
$txt->weight = get_string('weight', 'tool_mfa');
$txt->setup = get_string('setuprequired', 'tool_mfa');
$txt->input = get_string('inputrequired', 'tool_mfa');
$table = new \html_table();
$table->id = 'managemfatable';
$table->attributes['class'] = 'admintable generaltable';
$table->head = [
$txt->factor,
$txt->enable,
$txt->order,
$txt->weight,
$txt->settings,
$txt->setup,
$txt->input,
];
$table->colclasses = ['leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign'];
$table->data = [];
$factors = \tool_mfa\plugininfo\factor::get_factors();
$enabledfactors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$order = 1;
foreach ($factors as $factor) {
$settingsparams = ['section' => 'factor_'.$factor->name];
$settingsurl = new \moodle_url('settings.php', $settingsparams);
$settingslink = \html_writer::link($settingsurl, $txt->settings);
if ($factor->is_enabled()) {
$hideshowparams = ['action' => 'disable', 'factor' => $factor->name, 'sesskey' => $sesskey];
$hideshowurl = new \moodle_url('tool/mfa/index.php', $hideshowparams);
$hideshowlink = \html_writer::link($hideshowurl, $OUTPUT->pix_icon('t/hide', $txt->disable));
$class = '';
if ($order > 1) {
$upparams = ['action' => 'up', 'factor' => $factor->name, 'sesskey' => $sesskey];
$upurl = new \moodle_url('tool/mfa/index.php', $upparams);
$uplink = \html_writer::link($upurl, $OUTPUT->pix_icon('t/up', $txt->moveup));
} else {
$uplink = \html_writer::link('', $uplink = $OUTPUT->spacer(['style' => 'margin-right: .5rem']));
}
if ($order < count($enabledfactors)) {
$downparams = ['action' => 'down', 'factor' => $factor->name, 'sesskey' => $sesskey];
$downurl = new \moodle_url('tool/mfa/index.php', $downparams);
$downlink = \html_writer::link($downurl, $OUTPUT->pix_icon('t/down', $txt->movedown));
} else {
$downlink = '';
}
$updownlink = $uplink.$downlink;
$order++;
} else {
$hideshowparams = ['action' => 'enable', 'factor' => $factor->name, 'sesskey' => $sesskey];
$hideshowurl = new \moodle_url('tool/mfa/index.php', $hideshowparams);
$hideshowlink = \html_writer::link($hideshowurl, $OUTPUT->pix_icon('t/show', $txt->enable));
$class = 'dimmed_text';
$updownlink = '';
}
$hassetup = $factor->has_setup() ? get_string('yes') : get_string('no');
$hasinput = $factor->has_input() ? get_string('yes') : get_string('no');
$rowarray = [
$factor->get_display_name(),
$hideshowlink,
$updownlink,
$factor->get_weight(),
$settingslink,
$hassetup,
$hasinput,
];
$row = new \html_table_row($rowarray);
$row->attributes['class'] = $class;
$table->data[] = $row;
}
return \html_writer::table($table);
}
/**
* Defines supplementary table that shows available combinations of factors enough for successful authentication.
*
* @return string HTML code
*/
public function define_factor_combinations_table() {
global $OUTPUT;
$factors = \tool_mfa\plugininfo\factor::get_enabled_factors();
$combinations = $this->get_factor_combinations($factors, 0, count($factors) - 1);
if (empty($combinations)) {
return $OUTPUT->notification(get_string('error:notenoughfactors', 'tool_mfa'), 'notifyproblem');
}
$txt = get_strings(['combination', 'totalweight'], 'tool_mfa');
$table = new \html_table();
$table->id = 'managemfatable';
$table->attributes['class'] = 'admintable generaltable table table-bordered';
$table->head = [$txt->combination, $txt->totalweight];
$table->colclasses = ['leftalign', 'centeralign'];
$table->data = [];
$factorstringconnector = get_string('connector', 'tool_mfa');
foreach ($combinations as $combination) {
$factorstrings = array_map(static function(object_factor_base $factor): string {
return $factor->get_summary_condition() . ' <sup>' . $factor->get_weight() . '</sup>';
}, $combination['combination']);
$string = implode(" {$factorstringconnector} ", $factorstrings);
$table->data[] = new \html_table_row([$string, $combination['totalweight']]);
}
return \html_writer::table($table);
}
/**
* Recursive method to get all possible combinations of given factors.
* Output is filtered by combination total weight (should be greater than 100).
*
* @param array $allfactors initial array of factor objects
* @param int $start start position in initial array
* @param int $end end position in initial array
* @param int $totalweight total weight of combination
* @param array $combination combination candidate
* @param array $result array that includes combination total weight and subarray of factors combination
*
* @return array
*/
public function get_factor_combinations($allfactors, $start = 0, $end = 0,
$totalweight = 0, $combination = [], $result = []) {
if ($totalweight >= 100) {
// Ensure this is a valid combination before appending result.
$valid = true;
foreach ($combination as $factor) {
if (!$factor->check_combination($combination)) {
$valid = false;
}
}
if ($valid) {
$result[] = ['totalweight' => $totalweight, 'combination' => $combination];
}
return $result;
} else if ($start > $end) {
return $result;
}
$combinationnext = $combination;
$combinationnext[] = $allfactors[$start];
$result = $this->get_factor_combinations(
$allfactors,
$start + 1,
$end,
$totalweight + $allfactors[$start]->get_weight(),
$combinationnext,
$result);
$result = $this->get_factor_combinations(
$allfactors,
$start + 1,
$end,
$totalweight,
$combination,
$result);
return $result;
}
}
@@ -0,0 +1,67 @@
<?php
// 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/>.
namespace tool_mfa\local\factor;
/**
* Fallback factor class.
*
* @package tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class fallback extends object_factor_base {
/**
* Overridden constructor. Name is hard set to 'fallback'.
*/
public function __construct() {
$this->name = 'fallback';
}
/**
* {@inheritDoc}
*/
public function get_display_name(): string {
return get_string('fallback', 'tool_mfa');
}
/**
* {@inheritDoc}
*/
public function get_info(): string {
return get_string('fallback_info', 'tool_mfa');
}
/**
* {@inheritDoc}
*/
public function get_state(): string {
return \tool_mfa\plugininfo\factor::STATE_FAIL;
}
/**
* Sets the state of the factor check into the session.
* Returns whether storing the var was successful.
*
* @param string $state
* @return bool
*/
public function set_state(string $state): bool {
return false;
}
}
@@ -0,0 +1,331 @@
<?php
// 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/>.
/**
* MFA factor interface.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_mfa\local\factor;
use stdClass;
interface object_factor {
/**
* Returns true if factor is enabled, otherwise false.
*
* @return bool
* @throws \dml_exception
*/
public function is_enabled(): bool;
/**
* Returns configured factor weight.
*
* @return int
* @throws \dml_exception
*/
public function get_weight(): int;
/**
* Returns factor name from language string.
*
* @return string
* @throws \coding_exception
*/
public function get_display_name(): string;
/**
* Returns factor info from language string.
*
* @return string
* @throws \coding_exception
*/
public function get_info(): string;
/**
* Defines setup_factor form definition page for particular factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Defines setup_factor form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Implements setup_factor form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* @param array $data
* @return array
*/
public function setup_factor_form_validation(array $data): array;
/**
* Defines login form definition page for particular factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Defines login form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
* @throws \coding_exception
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm;
/**
* Implements login form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array;
/**
* Setups in given factor when the form is cancelled
*
* @param int $factorid
* @return void
*/
public function setup_factor_form_is_cancelled(int $factorid): void;
/**
* Setup submit button string in given factor
*
* @return string|null
*/
public function setup_factor_form_submit_button_string(): ?string;
/**
* Setups given factor and adds it to user's active factors list.
* Returns true if factor has been successfully added, otherwise false.
*
* @param stdClass $data
* @return stdClass|null the factor record, or null.
*/
public function setup_user_factor(stdClass $data): stdClass|null;
/**
* Returns an array of all user factors of given type (both active and revoked).
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array;
/**
* Returns an array of active user factor records.
* Filters get_all_user_factors() output.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_active_user_factors(stdClass $user): array;
/**
* Returns true if factor class has factor records that might be revoked.
* It means that user can revoke factor record from their profile.
*
* @return bool
*/
public function has_revoke(): bool;
/**
* Marks factor record as revoked.
* If factorid is not provided, revoke all instances of factor.
*
* @param int|null $factorid
* @return bool
*/
public function revoke_user_factor(?int $factorid = null): bool;
/**
* When validation code is correct - update lastverified field for given factor.
* If factor id is not provided, update all factor entries for user.
*
* @param int|null $factorid
* @return bool|\dml_exception
*/
public function update_lastverified(?int $factorid = null): bool|\dml_exception;
/**
* Gets lastverified timestamp.
*
* @param int $factorid
* @return int|bool
*/
public function get_lastverified(int $factorid): int|bool;
/**
* Returns true if factor needs to be setup by user and has setup_form.
*
* @return bool
*/
public function has_setup(): bool;
/**
* If has_setup returns true, decides if the setup buttons should be shown on the preferences page.
*
* @return bool
*/
public function show_setup_buttons(): bool;
/**
* Returns true if factor requires user input for success or failure during login.
*
* @return bool
*/
public function has_input(): bool;
/**
* Returns the state of the factor check
*
* @return string
*/
public function get_state(): string;
/**
* Sets the state of the factor check into the session.
* Returns whether storing the var was successful.
*
* @param string $state
* @return bool
*/
public function set_state(string $state): bool;
/**
* Fires any additional actions required by the factor once the user reaches the pass state.
*
* @return void
*/
public function post_pass_state(): void;
/**
* Retrieves label for a factorid.
*
* @param int $factorid
* @return string|\dml_exception
*/
public function get_label(int $factorid): string|\dml_exception;
/**
* Returns a list of urls to not redirect from.
*
* @return array
*/
public function get_no_redirect_urls(): array;
/**
* Returns all possible states for a user.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array;
/**
* Return summary condition for passing factor.
*
* @return string
*/
public function get_summary_condition(): string;
/**
* Checks whether the factor combination is valid based on factor behaviour.
* E.g. a combination with nosetup and another factor is not valid,
* as you cannot pass nosetup with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool;
/**
* Gets the string for setup button on preferences page.
*
* @return string the string to display on the button.
*/
public function get_setup_string(): string;
/**
* Deletes all instances of a factor for user.
*
* @param stdClass $user the user to delete for.
* @return void
*/
public function delete_factor_for_user(stdClass $user): void;
/**
* Process a cancel action from a user.
*
* @return void
*/
public function process_cancel_action(): void;
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition(\MoodleQuickForm $mform): void;
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition_after_data(\MoodleQuickForm $mform): void;
/**
* Hook point for global auth form action hooks.
*
* @param array $data Data from the form.
* @param array $files Files form the form.
* @return array of errors from validation.
*/
public function global_validation(array $data, array $files): array;
/**
* Hook point for global auth form action hooks.
*
* @param object $data Data from the form.
* @return void
*/
public function global_submit(object $data): void;
}
@@ -0,0 +1,715 @@
<?php
// 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/>.
namespace tool_mfa\local\factor;
use stdClass;
/**
* MFA factor abstract class.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class object_factor_base implements object_factor {
/** @var string Factor name */
public $name;
/** @var int Lock counter */
private $lockcounter;
/**
* Secret manager
*
* @var \tool_mfa\local\secret_manager
*/
protected $secretmanager;
/** @var string Factor icon */
protected $icon = 'fa-lock';
/**
* Class constructor
*
* @param string $name factor name
*/
public function __construct($name) {
global $DB, $USER;
$this->name = $name;
// Setup secret manager.
$this->secretmanager = new \tool_mfa\local\secret_manager($this->name);
}
/**
* This loads the locked state from the DB
* Base class implementation.
*
* @return void
*/
public function load_locked_state(): void {
global $DB, $USER;
// Check if lockcounter column exists (incase upgrade hasnt run yet).
// Only 'input factors' are lockable.
if ($this->is_enabled() && $this->is_lockable()) {
try {
// Setup the lock counter.
$sql = "SELECT MAX(lockcounter) FROM {tool_mfa} WHERE userid = ? AND factor = ? AND revoked = ?";
@$this->lockcounter = $DB->get_field_sql($sql, [$USER->id, $this->name, 0]);
if (empty($this->lockcounter)) {
$this->lockcounter = 0;
}
// Now lock this factor if over the counter.
$lockthreshold = get_config('tool_mfa', 'lockout');
if ($this->lockcounter >= $lockthreshold) {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
}
} catch (\dml_exception $e) {
// Set counter to -1.
$this->lockcounter = -1;
}
}
}
/**
* Returns true if factor is enabled, otherwise false.
*
* Base class implementation.
*
* @return bool
* @throws \dml_exception
*/
public function is_enabled(): bool {
$status = get_config('factor_'.$this->name, 'enabled');
if ($status == 1) {
return true;
}
return false;
}
/**
* Returns configured factor weight.
*
* Base class implementation.
*
* @return int
* @throws \dml_exception
*/
public function get_weight(): int {
$weight = get_config('factor_'.$this->name, 'weight');
if ($weight) {
return (int) $weight;
}
return 0;
}
/**
* Returns factor name from language string.
*
* Base class implementation.
*
* @return string
* @throws \coding_exception
*/
public function get_display_name(): string {
return get_string('pluginname', 'factor_'.$this->name);
}
/**
* Returns factor help from language string.
*
* Base class implementation.
*
* @return string
* @throws \coding_exception
*/
public function get_info(): string {
return get_string('info', 'factor_'.$this->name);
}
/**
* Returns factor help from language string when there is factor management available.
*
* Base class implementation.
*
* @param int $factorid The factor we want manage info for.
* @return string
* @throws \coding_exception
*/
public function get_manage_info(int $factorid): string {
return get_string('manageinfo', 'factor_'.$this->name, $this->get_label($factorid));
}
/**
* Defines setup_factor form definition page for particular factor.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Defines setup_factor form definition page after form data has been set.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Implements setup_factor form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* Dummy implementation. Should be overridden in child class.
*
* @param array $data
* @return array
*/
public function setup_factor_form_validation(array $data): array {
return [];
}
/**
* Setups in given factor when the form is cancelled
*
* Dummy implementation. Should be overridden in child class.
*
* @param int $factorid
* @return void
*/
public function setup_factor_form_is_cancelled(int $factorid): void {
}
/**
* Setup submit button string in given factor
*
* Dummy implementation. Should be overridden in child class.
*
* @return string|null
*/
public function setup_factor_form_submit_button_string(): ?string {
return null;
}
/**
* Setups given factor and adds it to user's active factors list.
* Returns true if factor has been successfully added, otherwise false.
*
* Dummy implementation. Should be overridden in child class.
*
* @param stdClass $data
* @return stdClass|null the record if created, or null.
*/
public function setup_user_factor(stdClass $data): stdClass|null {
return null;
}
/**
* Replaces a given factor and adds it to user's active factors list.
* Returns the new factor if it has been successfully replaced.
*
* Dummy implementation. Should be overridden in child class.
*
* @param stdClass $data The new factor data.
* @param int $id The id of the factor to replace.
* @return stdClass|null the record if created, or null.
*/
public function replace_user_factor(stdClass $data, int $id): stdClass|null {
return null;
}
/**
* Returns an array of all user factors of given type (both active and revoked).
*
* Dummy implementation. Should be overridden in child class.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
return [];
}
/**
* Returns an array of active user factor records.
* Filters get_all_user_factors() output.
*
* @param stdClass $user object to check against.
* @return array
*/
public function get_active_user_factors(stdClass $user): array {
$return = [];
$factors = $this->get_all_user_factors($user);
foreach ($factors as $factor) {
if ($factor->revoked == 0) {
$return[] = $factor;
}
}
return $return;
}
/**
* Defines login form definition page for particular factor.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Defines login form definition page after form data has been set.
*
* Dummy implementation. Should be overridden in child class.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
return $mform;
}
/**
* Implements login form validation for particular factor.
* Returns an array of errors, where array key = field id and array value = error text.
*
* Dummy implementation. Should be overridden in child class.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
return [];
}
/**
* Returns true if factor class has factor records that might be revoked.
* It means that user can revoke factor record from their profile.
*
* Override in child class if necessary.
*
* @return bool
*/
public function has_revoke(): bool {
return false;
}
/**
* Marks factor record as revoked.
* If factorid is not provided, revoke all instances of factor.
*
* @param int|null $factorid
* @return bool
* @throws \coding_exception
* @throws \dml_exception
*/
public function revoke_user_factor(?int $factorid = null): bool {
global $DB, $USER;
if (!empty($factorid)) {
// If we have an explicit factor id, this means we need to be careful about the user.
$params = ['id' => $factorid];
$existing = $DB->get_record('tool_mfa', $params);
if (empty($existing)) {
return false;
}
$matchinguser = $existing->userid == $USER->id;
if (!is_siteadmin() && !$matchinguser) {
// We aren't admin, and this isn't our factor.
return false;
}
} else {
$params = ['userid' => $USER->id, 'factor' => $this->name];
}
$DB->set_field('tool_mfa', 'revoked', 1, $params);
$event = \tool_mfa\event\user_revoked_factor::user_revoked_factor_event($USER, $this->get_display_name());
$event->trigger();
return true;
}
/**
* Returns true if factor class has factor records that can be replaced.
*
* Override in child class if necessary.
*
* @return bool
*/
public function has_replace(): bool {
return false;
}
/**
* When validation code is correct - update lastverified field for given factor.
* If factor id is not provided, update all factor entries for user.
*
* @param int|null $factorid
* @return bool|\dml_exception
* @throws \dml_exception
*/
public function update_lastverified(?int $factorid = null): bool|\dml_exception {
global $DB, $USER;
if (!empty($factorid)) {
$params = ['id' => $factorid];
} else {
$params = ['factor' => $this->name, 'userid' => $USER->id];
}
return $DB->set_field('tool_mfa', 'lastverified', time(), $params);
}
/**
* Gets lastverified timestamp.
*
* @param int $factorid
* @return int|bool the lastverified timestamp, or false if not found.
*/
public function get_lastverified(int $factorid): int|bool {
global $DB;
$record = $DB->get_record('tool_mfa', ['id' => $factorid]);
return $record->lastverified;
}
/**
* Returns true if factor needs to be setup by user and has setup_form.
* Override in child class if necessary.
*
* @return bool
*/
public function has_setup(): bool {
return false;
}
/**
* If has_setup returns true, decides if the setup buttons should be shown on the preferences page.
*
* @return bool
*/
public function show_setup_buttons(): bool {
return $this->has_setup();
}
/**
* Returns true if a factor requires input from the user to verify.
*
* Override in child class if necessary
*
* @return bool
*/
public function has_input(): bool {
return true;
}
/**
* Returns true if a factor is able to be locked if it fails.
*
* Generally only input factors are lockable.
* Override in child class if necessary
*
* @return bool
*/
public function is_lockable(): bool {
return $this->has_input();
}
/**
* Returns the state of the factor from session information.
*
* Implementation for factors that require input.
* Should be overridden in child classes with no input.
*
* @return mixed
*/
public function get_state(): string {
global $SESSION;
$property = 'factor_'.$this->name;
if (property_exists($SESSION, $property)) {
return $SESSION->$property;
} else {
return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
}
}
/**
* Sets the state of the factor into the session.
*
* Implementation for factors that require input.
* Should be overridden in child classes with no input.
*
* @param string $state the state constant to set.
* @return bool
*/
public function set_state(string $state): bool {
global $SESSION;
// Do not allow overwriting fail states.
if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_FAIL) {
return false;
}
$property = 'factor_'.$this->name;
$SESSION->$property = $state;
return true;
}
/**
* Creates an event when user successfully setup a factor
*
* @param object $user
* @return void
*/
public function create_event_after_factor_setup(object $user): void {
$event = \tool_mfa\event\user_setup_factor::user_setup_factor_event($user, $this->get_display_name());
$event->trigger();
}
/**
* Function for factor actions in the pass state.
* Override in child class if necessary.
*
* @return void
*/
public function post_pass_state(): void {
// Update lastverified for factor.
if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS) {
$this->update_lastverified();
}
// Now clean temp secrets for factor.
$this->secretmanager->cleanup_temp_secrets();
}
/**
* Function to retrieve the label for a factorid.
*
* @param int $factorid
* @return string|\dml_exception
*/
public function get_label(int $factorid): string|\dml_exception {
global $DB;
return $DB->get_field('tool_mfa', 'label', ['id' => $factorid]);
}
/**
* Function to get urls that should not be redirected from.
*
* @return array
*/
public function get_no_redirect_urls(): array {
return [];
}
/**
* Function to get possible states for a user from factor.
* Implementation where state is based on deterministic user data.
* This should be overridden in factors where state is non-deterministic.
* E.g. IP changes based on whether a user is using a VPN.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [$this->get_state()];
}
/**
* Returns condition for passing factor.
* Implementation for basic conditions.
* Override for complex conditions such as auth type.
*
* @return string
*/
public function get_summary_condition(): string {
return get_string('summarycondition', 'factor_'.$this->name);
}
/**
* Checks whether the factor combination is valid based on factor behaviour.
* E.g. a combination with nosetup and another factor is not valid,
* as you cannot pass nosetup with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool {
return true;
}
/**
* Gets the string for setup button on preferences page.
*
* @return string
*/
public function get_setup_string(): string {
return get_string('setupfactor', 'tool_mfa');
}
/**
* Gets the string for manage button on preferences page.
*
* @return string
*/
public function get_manage_string(): string {
return get_string('managefactor', 'tool_mfa');
}
/**
* Deletes all instances of factor for a user.
*
* @param stdClass $user the user to delete for.
* @return void
*/
public function delete_factor_for_user(stdClass $user): void {
global $DB, $USER;
$DB->delete_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
// Emit event for deletion.
$event = \tool_mfa\event\user_deleted_factor::user_deleted_factor_event($user, $USER, $this->name);
$event->trigger();
}
/**
* Increments the lock counter for a factor.
*
* @return void
*/
public function increment_lock_counter(): void {
global $DB, $USER;
// First make sure the state is loaded.
$this->load_locked_state();
// If lockcounter is negative, the field does not exist yet.
if ($this->lockcounter === -1) {
return;
}
$this->lockcounter++;
// Update record in DB.
$DB->set_field('tool_mfa', 'lockcounter', $this->lockcounter, ['userid' => $USER->id, 'factor' => $this->name]);
// Now lock this factor if over the counter.
$lockthreshold = get_config('tool_mfa', 'lockout');
if ($this->lockcounter >= $lockthreshold) {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
}
}
/**
* Return the number of remaining attempts at this factor.
*
* @return int the number of attempts at this factor remaining.
*/
public function get_remaining_attempts(): int {
$lockthreshold = get_config('tool_mfa', 'lockout');
if ($this->lockcounter === -1) {
// If upgrade.php hasnt been run yet, just return 10.
return $lockthreshold;
} else {
return $lockthreshold - $this->lockcounter;
}
}
/**
* Process a cancel input from a user.
*
* @return void
*/
public function process_cancel_action(): void {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_NEUTRAL);
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition(\MoodleQuickForm $mform): void {
return;
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition_after_data(\MoodleQuickForm $mform): void {
return;
}
/**
* Hook point for global auth form action hooks.
*
* @param array $data Data from the form.
* @param array $files Files form the form.
* @return array of errors from validation.
*/
public function global_validation(array $data, array $files): array {
return [];
}
/**
* Hook point for global auth form action hooks.
*
* @param object $data Data from the form.
* @return void
*/
public function global_submit(object $data): void {
return;
}
/**
* Get the icon associated with this factor.
*
* @return string the icon name.
*/
public function get_icon(): string {
return $this->icon;
}
/**
* Get the login description associated with this factor.
* Override for factors that have a user input.
*
* @return string The login option.
*/
public function get_login_desc(): string {
return get_string('logindesc', 'factor_'.$this->name);
}
}
@@ -0,0 +1,56 @@
<?php
// 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/>.
namespace tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* Factor action confirmation form.
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_action_confirmation_form extends \moodleform {
/**
* Form definition.
*/
public function definition(): void {
$mform = $this->_form;
$factor = $this->_customdata['factor'];
$devicename = $this->_customdata['devicename'];
$factorid = $this->_customdata['factorid'];
$action = $this->_customdata['action'];
$mform->addElement('html', get_string('confirmation' . $action, 'tool_mfa', $devicename));
$mform->setType('factorid', PARAM_INT);
$mform->addElement('hidden', 'factorid', $factorid);
$mform->setType('factor', PARAM_TEXT);
$mform->addElement('hidden', 'factor', $factor);
$mform->setType('action', PARAM_TEXT);
$mform->addElement('hidden', 'action', $action);
$mform->addElement('hidden', 'sesskey', sesskey());
}
}
@@ -0,0 +1,90 @@
<?php
// 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/>.
namespace tool_mfa\local\form;
use tool_mfa\plugininfo\factor;
/**
* MFA login form
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class global_form_manager {
/** @var array factors to call hooks upon. */
private $activefactors;
/**
* Create an instance of this class.
*/
public function __construct() {
$this->activefactors = factor::get_active_user_factor_types();
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function definition(\MoodleQuickForm &$mform): void {
foreach ($this->activefactors as $factor) {
$factor->global_definition($mform);
}
}
/**
* Hook point for global auth form action hooks.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function definition_after_data(\MoodleQuickForm &$mform): void {
foreach ($this->activefactors as $factor) {
$factor->global_definition_after_data($mform);
}
}
/**
* Hook point for global auth form action hooks.
*
* @param array $data Data from the form.
* @param array $files Files form the form.
* @return array of errors from validation.
*/
public function validation(array $data, array $files): array {
$errors = [];
foreach ($this->activefactors as $factor) {
$errors = array_merge($errors, $factor->global_validation($data, $files));
}
return $errors;
}
/**
* Hook point for global auth form submission hooks.
*
* @param \stdClass $data Data from the form.
* @return void
*/
public function submit(\stdClass $data): void {
foreach ($this->activefactors as $factor) {
$factor->global_submit($data);
}
}
}
@@ -0,0 +1,156 @@
<?php
// 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/>.
namespace tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* MFA login form
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class login_form extends \moodleform {
/** @var \tool_mfa\local\form\global_form_manager */
public $globalmanager;
/**
* Create an instance of this class.
*
* @param mixed $action the action attribute for the form. If empty defaults to auto detect the
* current url. If a moodle_url object then outputs params as hidden variables.
* @param mixed $customdata if your form defintion method needs access to data such as $course
* $cm, etc. to construct the form definition then pass it in this array. You can
* use globals for somethings.
* @param string $method if you set this to anything other than 'post' then _GET and _POST will
* be merged and used as incoming data to the form.
* @param string $target target frame for form submission. You will rarely use this. Don't use
* it if you don't need to as the target attribute is deprecated in xhtml strict.
* @param mixed $attributes you can pass a string of html attributes here or an array.
* Special attribute 'data-random-ids' will randomise generated elements ids. This
* is necessary when there are several forms on the same page.
* Special attribute 'data-double-submit-protection' set to 'off' will turn off
* double-submit protection JavaScript - this may be necessary if your form sends
* downloadable files in response to a submit button, and can't call
* \core_form\util::form_download_complete();
* @param bool $editable
* @param array $ajaxformdata Forms submitted via ajax, must pass their data here, instead of relying on _GET and _POST.
*/
public function __construct($action = null, $customdata = null, $method = 'post', $target = '',
$attributes = null, $editable = true, $ajaxformdata = null) {
$this->globalmanager = new \tool_mfa\local\form\global_form_manager();
parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata);
}
/**
* {@inheritDoc}
* @see moodleform::definition()
*/
public function definition(): void {
$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);
}
/**
* Invokes factor login_form_definition_after_data() method after form data has been set.
*
* @return void
*/
public function definition_after_data(): void {
$mform = $this->_form;
$factor = $this->_customdata['factor'];
$factor->login_form_definition_after_data($mform);
$this->globalmanager->definition_after_data($mform);
$buttonarray = [];
$buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('loginsubmit', 'factor_' . $factor->name));
$mform->addGroup($buttonarray, 'buttonar', '', [' '], false);
$mform->closeHeaderBefore('buttonar');
}
/**
* Validates the login form with given factor validation method.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
$factor = $this->_customdata['factor'];
$errors += $factor->login_form_validation($data);
$errors += $this->globalmanager->validation($data, $files);
// Execute sleep time bruteforce mitigation.
\tool_mfa\manager::sleep_timer();
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);
}
}
@@ -0,0 +1,98 @@
<?php
// 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/>.
namespace tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/formslib.php");
/**
* Form to reset gracemode timer for users.
*
* @package tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reset_factor extends \moodleform {
/**
* Form definition.
*/
public function definition(): void {
$mform = $this->_form;
$factors = $this->_customdata['factors'];
$bulkaction = $this->_customdata['bulk'];
$mform->addElement('hidden', 'bulkaction', $bulkaction);
$mform->setType('bulkaction', PARAM_BOOL);
$mform->addElement('hidden', 'returnurl');
$mform->setType('returnurl', PARAM_LOCALURL);
$factors = array_map(function ($element) {
return $element->get_display_name();
}, $factors);
// Add an 'all' action.
$factors['all'] = get_string('all');
$mform->addElement('select', 'factor', get_string('selectfactor', 'tool_mfa'), $factors);
if (!$bulkaction) {
$mform->addElement('text', 'resetfactor', get_string('resetuser', 'tool_mfa'),
['placeholder' => get_string('resetfactorplaceholder', 'tool_mfa')]);
$mform->setType('resetfactor', PARAM_TEXT);
$mform->addRule('resetfactor', get_string('userempty', 'tool_mfa'), 'required');
}
$this->add_action_buttons(true, get_string('resetconfirm', 'tool_mfa'));
}
/**
* Form validation.
*
* Server side rules do not work for uploaded files, implement serverside rules here if needed.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
global $DB;
$errors = parent::validation($data, $files);
if (!$data['bulkaction']) {
$userinfo = $data['resetfactor'];
// Try input as username first, then email.
$user = $DB->get_record('user', ['username' => $userinfo]);
if (empty($user)) {
// If not found, try username.
$user = $DB->get_record('user', ['email' => $userinfo]);
}
if (empty($user)) {
$errors['resetfactor'] = get_string('usernotfound', 'tool_mfa');
} else {
// Add custom field to store user.
$this->_form->addElement('hidden', 'user', $user);
$this->_form->setType('user', PARAM_RAW);
}
}
return $errors;
}
}
@@ -0,0 +1,100 @@
<?php
// 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/>.
namespace tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* Setup factor form
*
* @package tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class setup_factor_form extends \moodleform {
/**
* {@inheritDoc}
* @see moodleform::definition()
*/
public function definition(): void {
$mform = $this->_form;
// Indicate a factor id that will be replaced with this setup.
$replaceid = $this->_customdata['replaceid'] ?? null;
if (!empty($replaceid)) {
$mform->addelement('hidden', 'replaceid', $replaceid);
$mform->setType('replaceid', PARAM_INT);
}
$factorname = $this->_customdata['factorname'];
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$mform = $factor->setup_factor_form_definition($mform);
$this->xss_whitelist_static_form_elements($mform);
}
/**
* Validates setup_factor form with given factor validation method.
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
$factorname = $this->_customdata['factorname'];
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$errors += $factor->setup_factor_form_validation($data);
return $errors;
}
/**
* Invokes factor setup_factor_form_definition_after_data() method after form data has been set.
*/
public function definition_after_data(): void {
$mform = $this->_form;
$factorname = $this->_customdata['factorname'];
$factor = \tool_mfa\plugininfo\factor::get_factor($factorname);
$mform = $factor->setup_factor_form_definition_after_data($mform);
$this->xss_whitelist_static_form_elements($mform);
$this->add_action_buttons(true, $factor->setup_factor_form_submit_button_string());
}
/**
* Form elements clean up
*
* @param \HTML_QuickForm $mform
* @return void
*/
private function xss_whitelist_static_form_elements($mform): void {
if (!method_exists('MoodleQuickForm_static', 'set_allow_xss')) {
return;
}
$elements = $mform->_elements;
foreach ($elements as $element) {
if (is_a($element, 'MoodleQuickForm_static')) {
$element->set_allow_xss(true);
}
}
}
}
@@ -0,0 +1,124 @@
<?php
// 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/>.
namespace tool_mfa\local\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/form/text.php');
/**
* MFA Verification code element.
*
* @package tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class verification_field extends \MoodleQuickForm_text {
/** @var bool */
private $appendjs;
/**
* Verification field is a text entry box that features some useful extras.
*
* Contains JS to autosubmit the auth page when code is entered, as well as additional styling.
*
* @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, string $elementlabel = null) {
global $PAGE;
// Force attributes.
if (empty($attributes)) {
$attributes = [];
}
$attributes['autocomplete'] = 'one-time-code';
$attributes['inputmode'] = 'numeric';
$attributes['pattern'] = '[0-9]*';
// 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.
// We shouldn't autofocus here, as it probably isn't the only element, or main target.
if ($auth) {
$attributes['autofocus'] = 'autofocus';
}
// If we are on the auth page, load JS for element.
$this->appendjs = false;
if ($auth) {
$PAGE->requires->js_call_amd('tool_mfa/autosubmit_verification_code', 'init', []);
}
// Force element name to match JS.
$elementname = 'verificationcode';
// Overwrite default element label if set.
$elementlabel = !empty($elementlabel) ? $elementlabel : get_string('entercode', 'tool_mfa');
return parent::__construct($elementname, $elementlabel, $attributes);
}
/**
* Returns HTML for this form element.
*
* phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
*
* @return string
*/
public function toHtml(): string {
// Empty the value after all attributes decided.
$this->_attributes['value'] = '';
$result = parent::toHtml();
$submitjs = "<script>
document.querySelector('#id_verificationcode').addEventListener('keyup', function() {
if (this.value.length == 6) {
// Submits the closes form (parent).
this.closest('form').submit();
}
});
</script>";
if ($this->appendjs) {
$result .= $submitjs;
}
return $result;
}
/**
* Setup and return the script for autosubmission while inside the secure layout.
*
* @return string the JS to inline attach to the rendered object.
*/
public function secure_js(): string {
// Empty the value after all attributes decided.
$this->_attributes['value'] = '';
return "<script>
document.querySelector('#id_verificationcode').addEventListener('keyup', function() {
if (this.value.length == 6) {
// Submits the closes form (parent).
this.closest('form').submit();
}
});
</script>";
}
}
@@ -0,0 +1,41 @@
<?php
// 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/>.
namespace tool_mfa\local\hooks;
/**
* Extend user bulk actions menu
*
* @package tool_mfa
* @copyright 2024 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extend_bulk_user_actions {
/**
* Add action to reset MFA factors
*
* @param \core_user\hook\extend_bulk_user_actions $hook
*/
public static function callback(\core_user\hook\extend_bulk_user_actions $hook): void {
if (has_capability('moodle/site:config', \context_system::instance())) {
$hook->add_action('tool_mfa_reset_factors', new \action_link(
new \moodle_url('/admin/tool/mfa/reset_factor.php'),
get_string('resetfactor', 'tool_mfa')
));
}
}
}
@@ -0,0 +1,244 @@
<?php
// 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/>.
namespace tool_mfa\local;
/**
* MFA secret management class.
*
* @package tool_mfa
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class secret_manager {
/** @var string */
const REVOKED = 'revoked';
/** @var string */
const VALID = 'valid';
/** @var string */
const NONVALID = 'nonvalid';
/** @var string */
private $factor;
/** @var string|false */
private $sessionid;
/**
* Initialises a secret manager instance
*
* @param string $factor
*/
public function __construct(string $factor) {
$this->factor = $factor;
$this->sessionid = session_id();
}
/**
* This function creates or takes a secret, and stores it in the database or session.
*
* @param int $expires the length of time the secret is valid. e.g. 1 min = 60
* @param bool $session whether this secret should be linked to the session.
* @param string $secret an optional provided secret
* @return string the secret code, or 0 if no new code created.
*/
public function create_secret(int $expires, bool $session, string $secret = null): string {
// Check if there already an active secret, unless we are forcibly given a code.
if ($this->has_active_secret($session) && empty($secret)) {
return '';
}
// Setup a secret if not provided.
if (empty($secret)) {
$secret = random_int(100000, 999999);
}
// Now pass the code where it needs to go.
if ($session) {
$this->add_secret_to_db($secret, $expires, $this->sessionid);
} else {
$this->add_secret_to_db($secret, $expires);
}
return $secret;
}
/**
* Inserts the provided secret into the database with a given expiry duration.
*
* @param string $secret the secret to store
* @param int $expires expiry duration in seconds
* @param string $sessionid an optional sessionID to tie this record to
* @return void
*/
private function add_secret_to_db(string $secret, int $expires, string $sessionid = null): void {
global $DB, $USER;
$expirytime = time() + $expires;
$data = [
'userid' => $USER->id,
'factor' => $this->factor,
'secret' => $secret,
'timecreated' => time(),
'expiry' => $expirytime,
'revoked' => 0,
];
if (!empty($sessionid)) {
$data['sessionid'] = $sessionid;
}
$DB->insert_record('tool_mfa_secrets', $data);
}
/**
* Validates whether the provided secret is currently valid.
*
* @param string $secret the secret to check
* @param bool $keep should the secret be kept for reuse until expiry?
* @return string a secret manager state constant
*/
public function validate_secret(string $secret, bool $keep = false): string {
global $DB, $USER;
$status = $this->check_secret_against_db($secret, $this->sessionid);
if ($status !== self::NONVALID) {
if ($status === self::VALID && !$keep) {
// Cleanup DB $record.
$DB->delete_records('tool_mfa_secrets', ['userid' => $USER->id, 'factor' => $this->factor]);
}
return $status;
}
// This is always nonvalid.
return $status;
}
/**
* Checks if a given secret is valid from the Database.
*
* @param string $secret the secret to check.
* @param string $sessionid the session id to check for.
* @return string a secret manager state constant.
*/
private function check_secret_against_db(string $secret, string $sessionid): string {
global $DB, $USER;
$sql = "SELECT *
FROM {tool_mfa_secrets}
WHERE secret = :secret
AND expiry > :now
AND userid = :userid
AND factor = :factor";
$params = [
'secret' => $secret,
'now' => time(),
'userid' => $USER->id,
'factor' => $this->factor,
];
$record = $DB->get_record_sql($sql, $params);
if (!empty($record)) {
// If revoked it should always be revoked status.
if ($record->revoked) {
return self::REVOKED;
}
// Check if this is valid in only one session.
if (!empty($record->sessionid)) {
if ($record->sessionid === $sessionid) {
return self::VALID;
}
return self::NONVALID;
}
return self::VALID;
}
return self::NONVALID;
}
/**
* Revokes the provided secret code for the user.
*
* @param string $secret the secret to revoke.
* @param int $userid the userid to revoke the secret for.
* @return void
*/
public function revoke_secret(string $secret, $userid = null): void {
global $DB, $USER;
$userid = $userid ?? $USER->id;
// We do not need to worry about session vs global here.
// A factor should only ever use one.
// We know this secret is valid, so we don't need to check expiry.
$DB->set_field('tool_mfa_secrets', 'revoked', 1, ['userid' => $userid, 'factor' => $this->factor, 'secret' => $secret]);
}
/**
* Checks whether this factor currently has an active secret, and should not add another.
*
* @param bool $checksession should we only check if a current session secret is active?
* @return bool
*/
private function has_active_secret(bool $checksession = false): bool {
global $DB, $USER;
$sql = "SELECT *
FROM {tool_mfa_secrets}
WHERE expiry > :now
AND userid = :userid
AND factor = :factor
AND revoked = 0";
$params = [
'now' => time(),
'userid' => $USER->id,
'factor' => $this->factor,
];
if ($checksession) {
$sql .= ' AND sessionid = :sessionid';
$params['sessionid'] = $this->sessionid;
}
if ($DB->record_exists_sql($sql, $params)) {
return true;
}
return false;
}
/**
* Deletes any user secrets hanging around in the database.
*
* @param int $userid the userid to cleanup temp secrets for.
* @return void
*/
public function cleanup_temp_secrets($userid = null): void {
global $DB, $USER;
// Session records are autocleaned up.
// Only DB cleanup required.
$userid = $userid ?? $USER->id;
$sql = 'DELETE FROM {tool_mfa_secrets}
WHERE userid = :userid
AND factor = :factor';
$DB->execute($sql, ['userid' => $userid, 'factor' => $this->factor]);
}
}