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,93 @@
<?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 factor_admin;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Admin factor class.
*
* @package factor_admin
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* Admin Factor implementation.
* Factor is a singleton, can only be one instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Admin Factor implementation.
* Factor does not have input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Admin Factor implementation.
* State check is performed here, as there is no form to do it in.
*
* {@inheritDoc}
*/
public function get_state(): string {
if (is_siteadmin()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* Admin Factor implementation.
* The state can never be set. Always return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state($state): bool {
return true;
}
}
@@ -0,0 +1,40 @@
<?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 factor_admin\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_admin
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,30 @@
<?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/>.
/**
* Language strings.
*
* @package factor_admin
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['info'] = 'This factor allows for NOT being an administrator to count as a factor. Its intended use is to ensure administators require tighter security, so regular users get the weight for free, while admins must use other factors.';
$string['pluginname'] = 'Non-administrator';
$string['privacy:metadata'] = 'The Non-administrator factor plugin does not store any personal data.';
$string['settings:weight_help'] = 'Weight is given to regular users for this factor, so admins must have more factors than a regular user to pass.';
$string['summarycondition'] = 'is not an admin';
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Admin factor Settings.
*
* @package factor_admin
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_admin/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('admin', get_config('factor_admin', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_admin/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'factor_admin'), 100, PARAM_INT));
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_admin
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_admin'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,115 @@
<?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 factor_auth;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Auth factor class.
*
* @package factor_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* Auth Factor implementation.
* Factor is a singleton, can only be one instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Auth Factor implementation.
* Factor does not have input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Auth Factor implementation.
* State check is performed here, as there is no form to do it in.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$safetypes = get_config('factor_auth', 'goodauth');
if (strlen($safetypes) != 0) {
$safetypes = explode(',', $safetypes);
// Check all safetypes against user auth.
if (in_array($USER->auth, $safetypes, true)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
} else {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
/**
* Auth Factor implementation.
* The state can never be set. Always return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* Auth factor implementation.
* Return list of auth types that are safe.
*
* {@inheritDoc}
*/
public function get_summary_condition(): string {
$safetypes = get_config('factor_auth', 'goodauth');
return get_string('summarycondition', 'factor_'.$this->name, $safetypes);
}
}
@@ -0,0 +1,40 @@
<?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 factor_auth\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+55
View File
@@ -0,0 +1,55 @@
<?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/>.
/**
* factor_auth upgrade library.
*
* @package factor_auth
* @copyright 2021 Peter Burnett <peterburnett@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Factor auth upgrade helper function
*
* @param int $oldversion
*/
function xmldb_factor_auth_upgrade($oldversion) {
if ($oldversion < 2021020500) {
$authtypes = get_enabled_auth_plugins(true);
// Upgrade goodauth config from number to name.
$goodauth = explode(',', get_config('factor_auth', 'goodauth'));
$newauths = [];
foreach ($goodauth as $auth) {
// Check if index exists before access. If not, ignore, settings were out of sync.
if (array_key_exists($auth, $authtypes)) {
$newauths[] = $authtypes[$auth];
}
}
set_config('goodauth', implode(',', $newauths), 'factor_auth');
// MFA savepoint reached.
upgrade_plugin_savepoint(true, 2021020500, 'factor', 'auth');
}
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,31 @@
<?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/>.
/**
* Language strings.
*
* @package factor_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['info'] = 'Check the type of authentication used to log in as an MFA factor.';
$string['pluginname'] = 'Authentication type';
$string['privacy:metadata'] = 'The Authentication type factor plugin does not store any personal data.';
$string['settings:goodauth'] = 'Factor authentication types';
$string['settings:goodauth_help'] = 'Select all authentication types to use as a factor for MFA. Any types not selected will not be treated as a FAIL in MFA.';
$string['summarycondition'] = 'has an authentication type of {$a}';
+49
View File
@@ -0,0 +1,49 @@
<?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/>.
/**
* Settings
*
* @package factor_auth
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_auth/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('auth', get_config('factor_auth', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_auth/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$authtypes = get_enabled_auth_plugins(true);
$authselect = [];
foreach ($authtypes as $type) {
$auth = get_auth_plugin($type);
$authselect[$type] = $auth->get_title();
}
$settings->add(new admin_setting_configmulticheckbox('factor_auth/goodauth',
get_string('settings:goodauth', 'factor_auth'),
get_string('settings:goodauth_help', 'factor_auth'), [], $authselect));
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_auth
* @subpackage tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_auth'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,112 @@
<?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 factor_capability;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* User capability factor class.
*
* @package factor_capability
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* User capability implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* User capability implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* User capability implementation.
* Checks whether user has the negative capability.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$adminpass = (bool) get_config('factor_capability', 'adminpasses');
// Do anything check is controlled from factor config.
if (!has_capability('factor/capability:cannotpassfactor', \context_system::instance(), $USER, $adminpass)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
} else {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
/**
* User Capability implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* User capability implementation.
* Possible states are either neutral or pass.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
];
}
}
@@ -0,0 +1,40 @@
<?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 factor_capability\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_capability
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,33 @@
<?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/>.
/**
* User capability factor access declaration.
*
* @package factor_capability
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'factor/capability:cannotpassfactor' => [
'captype' => 'read',
'contextlevel' => CONTEXT_SYSTEM,
],
];
@@ -0,0 +1,33 @@
<?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/>.
/**
* Language strings.
*
* @package factor_capability
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['capability:cannotpassfactor'] = 'STOPS a role from passing the MFA user capability factor.';
$string['pluginname'] = 'User capability';
$string['privacy:metadata'] = 'The User capability factor plugin does not store any personal data.';
$string['settings:adminpasses'] = 'Site admins can pass this factor';
$string['settings:adminpasses_help'] = 'By default admins pass all capability checks, including this one which uses \'factor/capability:cannotpassfactor\', which means they will fail this factor.
If checked then all site admins will pass this factor if they do not have this capability from another role.
If unchecked site admins will fail this factor.';
$string['summarycondition'] = 'does NOT have the factor/capability:cannotpassfactor capability in any role including site administrator.';
@@ -0,0 +1,43 @@
<?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/>.
/**
* Settings
*
* @package factor_capability
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_capability/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('capability', get_config('factor_capability', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_capability/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
// Admin passes bool logic is inverted due to negative capability check.
$settings->add(new admin_setting_configcheckbox('factor_capability/adminpasses',
new lang_string('settings:adminpasses', 'factor_capability'),
new lang_string('settings:adminpasses_help', 'factor_capability'), 1, 0, 1));
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_capability
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_capability'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,153 @@
<?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 factor_cohort;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../../../cohort/lib.php');
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* cohort factor class.
*
* @package factor_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* cohort implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* cohort implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* cohort implementation.
* Checks whether the user has selected cohorts in any context.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$cohortstring = get_config('factor_cohort', 'cohorts');
// Nothing selected, everyone passes.
if (empty($cohortstring)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
$selected = explode(',', $cohortstring);
foreach ($selected as $id) {
if (cohort_is_member($id, $USER->id)) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
// If we got here, no cohorts matched, allow access.
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* cohort implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* cohort implementation.
* User can not influence. Result is whatever current state is.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
return [$this->get_state()];
}
/**
* cohort implementation
* Formats the cohort list nicely.
*
* {@inheritDoc}
*/
public function get_summary_condition(): string {
$selectedcohorts = get_config('factor_cohort', 'cohorts');
if (empty($selectedcohorts)) {
return get_string('summarycondition', 'factor_cohort', get_string('none'));
}
$selectedcohorts = $this->get_cohorts(explode(',', $selectedcohorts));
if (empty($selectedcohorts)) {
return get_string('summarycondition', 'factor_cohort', get_string('none'));
}
return get_string('summarycondition', 'factor_cohort', implode(', ', $selectedcohorts));
}
/**
* Get cohorts information by given ids.
*
* @param array $selectedcohorts List of cohort ids.
* @return array
*/
public function get_cohorts(array $selectedcohorts): array {
global $DB;
[$insql, $inparams] = $DB->get_in_or_equal($selectedcohorts);
$sql = "SELECT id, name FROM {cohort} WHERE id $insql";
$cohorts = $DB->get_records_sql_menu($sql, $inparams);
return $cohorts;
}
}
@@ -0,0 +1,40 @@
<?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 factor_cohort\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,30 @@
<?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/>.
/**
* Language strings.
*
* @package factor_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Cohort';
$string['privacy:metadata'] = 'The Cohort factor plugin does not store any personal data.';
$string['settings:cohort'] = 'Non-passing cohorts';
$string['settings:cohort_help'] = 'Select the cohorts that will not pass this factor. This allows you to force these cohorts to use other factors to authenticate.';
$string['summarycondition'] = 'does NOT have any of the following cohorts assigned in any context: {$a}';
+52
View File
@@ -0,0 +1,52 @@
<?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/>.
/**
* Settings
*
* @package factor_cohort
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../../../cohort/lib.php');
$enabled = new admin_setting_configcheckbox('factor_cohort/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('cohort', get_config('factor_cohort', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_cohort/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$cohorts = cohort_get_all_cohorts();
$choices = [];
foreach ($cohorts['cohorts'] as $cohort) {
$choices[$cohort->id] = $cohort->name;
}
if (!empty($choices)) {
$settings->add(new admin_setting_configmultiselect('factor_cohort/cohorts',
new lang_string('settings:cohort', 'factor_cohort'),
new lang_string('settings:cohort_help', 'factor_cohort'), [], $choices));
}
@@ -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 factor_cohort;
/**
* Tests for cohort factor.
*
* @covers \factor_cohort\factor
* @package factor_cohort
* @copyright 2023 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Tests getting the summary condition
*
* @covers ::get_summary_condition
* @covers ::get_cohorts
*/
public function test_get_summary_condition(): void {
$this->resetAfterTest();
set_config('enabled', 1, 'factor_cohort');
$cohortfactor = \tool_mfa\plugininfo\factor::get_factor('cohort');
$cohort = $this->getDataGenerator()->create_cohort();
$userassignover = $this->getDataGenerator()->create_user();
cohort_add_member($cohort->id, $userassignover->id);
// Add the created cohortid into factor_cohort plugin.
set_config('cohorts', $cohort->id, 'factor_cohort');
$selectedcohorts = get_config('factor_cohort', 'cohorts');
$selectedcohorts = $cohortfactor->get_cohorts(explode(',', $selectedcohorts));
$this->assertArrayHasKey($cohort->id, $selectedcohorts);
$this->assertStringContainsString(
implode(', ', $selectedcohorts),
$cohortfactor->get_summary_condition()
);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_cohort
* @subpackage tool_mfa
* @author Chris Pratt <tonyyeb@gmail.com>
* @copyright Chris Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_cohort'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,93 @@
<?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 factor_email\event;
use stdClass;
/**
* Event for when a user receives an unauthorised email from MFA.
*
* @property-read array $other {
* Extra information about event.
* }
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class unauth_email extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who passed all MFA factor checks.
* @param string $ip the ip address the unauthorised email came from.
* @param string $useragent the browser fingerpring the unauthorised email came from.
*
* @return \core\event\base the user_passed_mfa event
*
* @throws \coding_exception
*/
public static function unauth_email_event(stdClass $user, string $ip, string $useragent): \core\event\base {
$data = [
'relateduserid' => null,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'ip' => $ip,
'useragent' => $useragent,
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
$data = new stdClass();
$data->userid = $this->other['userid'];
$data->ip = $this->other['ip'];
$data->useragent = $this->other['useragent'];
return get_string('unauthloginattempt', 'factor_email', $data);
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:unauthemail', 'factor_email');
}
}
@@ -0,0 +1,338 @@
<?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 factor_email;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Email factor class.
*
* @package factor_email
* @subpackage 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 extends object_factor_base {
/** @var string Factor icon */
protected $icon = 'fa-envelope';
/**
* E-Mail Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
$mform->addElement(new \tool_mfa\local\form\verification_field());
$mform->setType('verificationcode', PARAM_ALPHANUM);
return $mform;
}
/**
* E-Mail Factor implementation.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
$this->generate_and_email_code();
return $mform;
}
/**
* Sends and e-mail to user with given verification code.
*
* @param int $instanceid
* @return void
*/
public static function email_verification_code(int $instanceid): void {
global $PAGE, $USER;
$noreplyuser = \core_user::get_noreply_user();
$subject = get_string('email:subject', 'factor_email');
$renderer = $PAGE->get_renderer('factor_email');
$body = $renderer->generate_email($instanceid);
email_to_user($USER, $noreplyuser, $subject, $body, $body);
}
/**
* E-Mail Factor implementation.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
global $USER;
$return = [];
if (!$this->check_verification_code($data['verificationcode'])) {
$return['verificationcode'] = get_string('error:wrongverification', 'factor_email');
}
return $return;
}
/**
* E-Mail Factor implementation.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', [
'userid' => $user->id,
'factor' => $this->name,
'label' => $user->email,
]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'label' => $user->email,
'createdfromip' => $user->lastip,
'timecreated' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* E-Mail Factor implementation.
*
* {@inheritDoc}
*/
public function has_input(): bool {
if (self::is_ready()) {
return true;
}
return false;
}
/**
* E-Mail Factor implementation.
*
* {@inheritDoc}
*/
public function get_state(): string {
if (!self::is_ready()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
return parent::get_state();
}
/**
* Checks whether user email is correctly configured.
*
* @return bool
*/
private static function is_ready(): bool {
global $DB, $USER;
if (empty($USER->email)) {
return false;
}
if (!validate_email($USER->email)) {
return false;
}
if (over_bounce_threshold($USER)) {
return false;
}
// If this factor is revoked, set to not ready.
if ($DB->record_exists('tool_mfa', ['userid' => $USER->id, 'factor' => 'email', 'revoked' => 1])) {
return false;
}
return true;
}
/**
* Generates and emails the code for login to the user, stores codes in DB.
*
* @return void
*/
private function generate_and_email_code(): void {
global $DB, $USER;
// Get instance that isnt parent email type (label check).
// This check must exclude the main singleton record, with the label as the email.
// It must only grab the record with the user agent as the label.
$sql = 'SELECT *
FROM {tool_mfa}
WHERE userid = ?
AND factor = ?
AND NOT label = ?';
$record = $DB->get_record_sql($sql, [$USER->id, 'email', $USER->email]);
$duration = get_config('factor_email', 'duration');
$newcode = random_int(100000, 999999);
if (empty($record)) {
// No code active, generate new code.
$instanceid = $DB->insert_record('tool_mfa', [
'userid' => $USER->id,
'factor' => 'email',
'secret' => $newcode,
'label' => $_SERVER['HTTP_USER_AGENT'],
'timecreated' => time(),
'createdfromip' => $USER->lastip,
'timemodified' => time(),
'lastverified' => time(),
'revoked' => 0,
], true);
$this->email_verification_code($instanceid);
} else if ($record->timecreated + $duration < time()) {
// Old code found. Keep id, update fields.
$DB->update_record('tool_mfa', [
'id' => $record->id,
'secret' => $newcode,
'label' => $_SERVER['HTTP_USER_AGENT'],
'timecreated' => time(),
'createdfromip' => $USER->lastip,
'timemodified' => time(),
'lastverified' => time(),
'revoked' => 0,
]);
$instanceid = $record->id;
$this->email_verification_code($instanceid);
}
}
/**
* Verifies entered code against stored DB record.
*
* @param string $enteredcode
* @return bool
*/
private function check_verification_code(string $enteredcode): bool {
global $DB, $USER;
$duration = get_config('factor_email', 'duration');
// Get instance that isnt parent email type (label check).
// This check must exclude the main singleton record, with the label as the email.
// It must only grab the record with the user agent as the label.
$sql = 'SELECT *
FROM {tool_mfa}
WHERE userid = ?
AND factor = ?
AND NOT label = ?';
$record = $DB->get_record_sql($sql, [$USER->id, 'email', $USER->email]);
if ($enteredcode == $record->secret) {
if ($record->timecreated + $duration > time()) {
return true;
}
}
return false;
}
/**
* Cleans up email records once MFA passed.
*
* {@inheritDoc}
*/
public function post_pass_state(): void {
global $DB, $USER;
// Delete all email records except base record.
$selectsql = 'userid = ?
AND factor = ?
AND NOT label = ?';
$DB->delete_records_select('tool_mfa', $selectsql, [$USER->id, 'email', $USER->email]);
// Update factor timeverified.
parent::post_pass_state();
}
/**
* Email factor implementation.
* Email page must be safe to authorise session from link.
*
* {@inheritDoc}
*/
public function get_no_redirect_urls(): array {
$email = new \moodle_url('/admin/tool/mfa/factor/email/email.php');
return [$email];
}
/**
* Email factor implementation.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
// Email can return all states.
return [
\tool_mfa\plugininfo\factor::STATE_FAIL,
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
\tool_mfa\plugininfo\factor::STATE_UNKNOWN,
];
}
/**
* Obscure an email address by replacing all but the first and last character of the local part with a dot.
* So the users full email isn't displayed during login.
*
* @param string $email The email address to obfuscate.
* @return string
* @throws \coding_exception
*/
protected function obfuscate_email(string $email): string {
// Split the email address at the '@' symbol.
$parts = explode('@', $email);
if (count($parts) != 2) {
throw new \coding_exception('Invalid email format');
}
$local = $parts[0];
$domain = $parts[1];
// Obfuscate all but the first and last character of the local part.
$length = strlen($local);
$middledot = "\u{00B7}";
if ($length > 2) {
$local = $local[0] . str_repeat($middledot, $length - 2) . $local[$length - 1];
}
// Put the email address back together and return it.
return $local . '@' . $domain;
}
/**
* 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 {
global $USER;
$email = $this->obfuscate_email($USER->email);
return get_string('logindesc', 'factor_' . $this->name, $email);
}
}
@@ -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 factor_email\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . "/formslib.php");
/**
* Revoke email form.
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class email extends \moodleform {
/**
* Form definition.
*/
public function definition(): void {
$mform = $this->_form;
$mform->addElement('html', get_string('email:accident', 'factor_email'));
$this->add_action_buttons(true, get_string('continue'));
}
/**
* 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): array {
$errors = parent::validation($data, $files);
return $errors;
}
}
@@ -0,0 +1,64 @@
<?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/>.
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/iplookup/lib.php');
/**
* Email renderer.
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_email_renderer extends plugin_renderer_base {
/**
* Generates an email
*
* @param int $instanceid
* @return string|boolean
*/
public function generate_email(int $instanceid): string|bool {
global $DB, $USER, $CFG;;
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$site = get_site();
$validity = get_config('factor_email', 'duration');
$authurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
['instance' => $instance->id, 'pass' => 1, 'secret' => $instance->secret]);
$authurlstring = \html_writer::link($authurl, get_string('email:link', 'factor_email'));
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php', ['instance' => $instanceid]);
$blockurlstring = \html_writer::link($blockurl, get_string('email:stoploginlink', 'factor_email'));
$geoinfo = iplookup_find_location($instance->createdfromip);
$templateinfo = [
'logo' => $this->get_compact_logo_url(100, 100),
'name' => $USER->firstname,
'sitename' => $site->fullname,
'siteurl' => $CFG->wwwroot,
'code' => $instance->secret,
'validity' => format_time($validity),
'authlink' => get_string('email:loginlink', 'factor_email', $authurlstring),
'revokelink' => get_string('email:revokelink', 'factor_email', $blockurlstring),
'ip' => $instance->createdfromip,
'geocity' => $geoinfo['city'],
'geocountry' => $geoinfo['country'],
'ua' => $instance->label,
];
return $this->render_from_template('factor_email/email', $templateinfo);
}
}
@@ -0,0 +1,40 @@
<?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 factor_email\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_email
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+106
View File
@@ -0,0 +1,106 @@
<?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/>.
/**
* Page to revoke and disable an email code.
*
* @package factor_email
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// Ignore coding standards for login check, this page does not require login.
// phpcs:disable moodle.Files.RequireLogin.Missing
require_once(__DIR__ . '/../../../../../config.php');
$instanceid = required_param('instance', PARAM_INT);
$pass = optional_param('pass', '0', PARAM_INT);
$secret = optional_param('secret', 0, PARAM_INT);
$context = context_system::instance();
$PAGE->set_context($context);
$url = new moodle_url('/admin/tool/mfa/factor/email/email.php',
['instance' => $instanceid, 'pass' => $pass, 'secret' => $secret]);
$PAGE->set_url($url);
$PAGE->set_pagelayout('secure');
$PAGE->set_title(get_string('unauthemail', 'factor_email'));
$PAGE->set_cacheable(false);
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
// If pass is set, require login to force $SESSION and user, and pass for that session.
if (!empty($instance) && $pass != 0 && $secret != 0) {
require_login();
if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
// Redirect through to auth, this will bounce them to the next factor.
redirect(new moodle_url('/admin/tool/mfa/auth.php'));
}
// Check the code with the same measures on the page entry.
if ($instance->secret != $secret) {
\tool_mfa\manager::sleep_timer();
$factor->increment_lock_counter();
throw new moodle_exception('error:parameters', 'factor_email');
}
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
$factor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
// If wantsurl is already set in session, go to it.
if (!empty($SESSION->wantsurl)) {
redirect($SESSION->wantsurl);
} else {
redirect(new moodle_url('/'));
}
}
$form = new \factor_email\form\email($url);
if ($form->is_cancelled()) {
redirect(new moodle_url('/'));
} else if ($fromform = $form->get_data()) {
if (empty($instance)) {
$message = get_string('error:badcode', 'factor_email');
} else {
$user = $DB->get_record('user', ['id' => $instance->userid]);
// Stop attacker from using email factor at all, by revoking all email until admin fixes.
$DB->set_field('tool_mfa', 'revoked', 1, ['userid' => $user->id, 'factor' => 'email']);
// Remotely logout all sessions for user.
$manager = \core\session\manager::kill_user_sessions($instance->userid);
// Log event.
$ip = $instance->createdfromip;
$useragent = $instance->label;
$event = \factor_email\event\unauth_email::unauth_email_event($user, $ip, $useragent);
$event->trigger();
// Suspend user account.
if (get_config('factor_email', 'suspend')) {
$DB->set_field('user', 'suspended', 1, ['id' => $user->id]);
}
$message = get_string('email:revokesuccess', 'factor_email', fullname($user));
}
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('unauthemail', 'factor_email'));
if (!empty($message)) {
echo $message;
} else {
$form->display();
}
echo $OUTPUT->footer();
@@ -0,0 +1,66 @@
<?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/>.
/**
* Language strings.
*
* @package factor_email
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['email:accident'] = 'If you didn\'t request the email, click continue to invalidate the login attempt. If you clicked the link by accident, click cancel, and no action will be taken.';
$string['email:browseragent'] = 'The browser details for this request are: \'{$a}\'';
$string['email:geoinfo'] = 'This request appears to have originated from approximately:';
$string['email:greeting'] = 'Hello {$a} &#128075;';
$string['email:ipinfo'] = 'Login request details:';
$string['email:link'] = 'verification link';
$string['email:loginlink'] = 'Or, if you\'re on the same device, use this {$a}.';
$string['email:message'] = 'Here\'s your verification code for {$a->sitename} ({$a->siteurl}).';
$string['email:originatingip'] = 'This login request was made from \'{$a}\'';
$string['email:revokelink'] = 'If this wasn\'t you, you can {$a}.';
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for {$a} have been ended.
Email will not be usable as a factor until account security has been verified.';
$string['email:subject'] = 'Here\'s your verification code';
$string['email:stoploginlink'] = 'stop this login attempt';
$string['email:uadescription'] = 'Browser identity for this request:';
$string['email:validity'] = 'The code can only be used once and is valid for {$a}.';
$string['error:badcode'] = 'Code was not found. This may be an old link, a new code may have been emailed, or the login attempt with this code was successful.';
$string['error:parameters'] = 'Incorrect page parameters.';
$string['error:wrongverification'] = 'Wrong code. Try again.';
$string['event:unauthemail'] = 'Unauthorised email received';
$string['info'] = 'You are using email {$a} to authenticate. This has been set up by your site administrator.';
$string['logindesc'] = 'We\'ve just sent a 6-digit code to your email: {$a}';
$string['loginoption'] = 'Have a code emailed to you';
$string['loginskip'] = "I didn't receive a code";
$string['loginsubmit'] = 'Continue';
$string['logintitle'] = "Verify it's you by email";
$string['managefactor'] = 'Manage email';
$string['manageinfo'] = '\'{$a}\' is being used to authenticate. This has been set up by your administrator.';
$string['pluginname'] = 'Email';
$string['privacy:metadata'] = 'The Email factor plugin does not store any personal data';
$string['settings:duration'] = 'Validity duration';
$string['settings:duration_help'] = 'The period of time that the code is valid.';
$string['settings:suspend'] = 'Suspend unauthorised accounts';
$string['settings:suspend_help'] = 'Check this to suspend user accounts if an unauthorised email verification is received.';
$string['setupfactor'] = 'Set up email';
$string['summarycondition'] = 'has valid email setup';
$string['unauthloginattempt'] = 'The user with ID {$a->userid} made an unauthorised login attempt using email verification from
IP {$a->ip} with browser agent {$a->useragent}.';
$string['unauthemail'] = 'Unauthorised email';
$string['verificationcode'] = 'Enter verification code for confirmation';
$string['verificationcode_help'] = 'A verification code has been sent to your email.';
+46
View File
@@ -0,0 +1,46 @@
<?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/>.
/**
* Settings
*
* @package factor_email
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_email/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('email', get_config('factor_email', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_email/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configduration('factor_email/duration',
get_string('settings:duration', 'factor_email'),
get_string('settings:duration_help', 'factor_email'), 30 * MINSECS, MINSECS));
$settings->add(new admin_setting_configcheckbox('factor_email/suspend',
get_string('settings:suspend', 'factor_email'),
get_string('settings:suspend_help', 'factor_email'), 0));
@@ -0,0 +1,65 @@
{{!
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 factor_email/email
Template for email body sent to users.
Example context (json):
{
"message": "Your verification code is 123456.",
"ip": "The IP was 127.0.0.1",
"geoinfo": "Request originated from Brisbane, Australia",
"ua": "The user agent is Firefox 70",
"linkstring": "If this wasn't you, click here"
}
}}
<div style="font-family: Arial, sans-serif; font-size: 18px">
{{#logo}}
<table style="width: 600px; margin-bottom: 10px;">
<tr>
<td></td>
<td style="text-align: right;">
<img src="{{{logo}}}" alt="{{sitename}}" />
</td>
</tr>
</table>
{{/logo}}
<p>{{#str}} email:greeting, factor_email, {{name}}{{/str}}</p>
<p>
{{#str}} email:message, factor_email, {"sitename":{{#quote}}{{sitename}}{{/quote}}, "siteurl":{{#quote}}{{siteurl}}{{/quote}} }{{/str}}
</p>
<h2 style="letter-spacing: 5px;">{{code}}</h2>
<p>
{{#str}} email:validity, factor_email, {{validity}}{{/str}}
<br/>
{{{authlink}}}
</p>
<br/>
<p>{{{revokelink}}}</p>
<br/>
<div style="font-family: Arial, sans-serif; font-size: 12px">
<p><strong>{{#str}} email:ipinfo, factor_email {{/str}}</strong></p>
<p>{{#str}} email:originatingip, factor_email, {{ip}} {{/str}}</p>
{{#geocountry}}
<p> {{#str}} email:geoinfo, factor_email {{/str}}{{#geocity}}{{geocity}},{{/geocity}} {{geocountry}}</p>
{{/geocountry}}
<p><strong>{{#str}} email:uadescription, factor_email {{/str}}</strong></p>
<p>{{ua}}</p>
<p>{{{linkstring}}}</p>
</div>
</div>
@@ -0,0 +1,84 @@
<?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 factor_email;
/**
* Tests for email factor.
*
* @covers \factor_email\factor
* @package factor_email
* @copyright 2023 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Tests checking verification code
*
* @covers ::check_verification_code
* @covers ::post_pass_state
*/
public function test_check_verification_code(): void {
global $DB, $USER;
$this->resetAfterTest(true);
$emailfactorclass = new \factor_email\factor('email');
$rc = new \ReflectionClass($emailfactorclass::class);
$rcm = $rc->getMethod('check_verification_code');
// Assigned email to be used in getting the email factor.
$USER->email = 'user@mail.com';
set_config('enabled', 1, 'factor_email');
// Testing with current timecreated.
$newcode = random_int(100000, 999999);
$instanceid = $DB->insert_record('tool_mfa', [
'userid' => $USER->id,
'factor' => 'email',
'secret' => $newcode,
'label' => 'unittest',
'timecreated' => time(),
'timemodified' => time(),
'lastverified' => time(),
'revoked' => 0,
]);
$data = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$this->assertTrue($rcm->invoke($emailfactorclass, $data->secret));
// Update the data to test with really old timecreated.
$DB->update_record('tool_mfa', [
'id' => $instanceid,
'timecreated' => time() - 1689657581,
'timemodified' => time() - 1689657581,
'lastverified' => time() - 1689657581,
'revoked' => 0,
]);
$data = $DB->get_record('tool_mfa', ['id' => $instanceid]);
$this->assertFalse($rcm->invoke($emailfactorclass, $data->secret));
// Cleans up email records once MFA passed.
$rcm = $rc->getMethod('post_pass_state');
$rcm->invoke($emailfactorclass);
// Check if the email records have been deleted.
$data = $DB->count_records('tool_mfa', ['factor' => 'email']);
$this->assertEquals(0, $data);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_email
* @subpackage tool_mfa
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_email'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,307 @@
<?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 factor_grace;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Grace period factor class.
*
* @package factor_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* Grace Factor implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'createdfromip' => $user->lastip,
'timecreated' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Grace Factor implementation.
* Singleton instance, no additional filtering needed.
*
* @param stdClass $user object to check against.
* @return array the array of active factors.
*/
public function get_active_user_factors(stdClass $user): array {
return $this->get_all_user_factors($user);
}
/**
* Grace Factor implementation.
* Factor has no input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Grace Factor implementation.
* Checks the user login time against their first login after MFA activation.
*
* @param bool $redirectable should this state call be allowed to redirect the user?
* @return string state constant
*/
public function get_state($redirectable = true): string {
global $FULLME, $SESSION, $USER;
$records = ($this->get_all_user_factors($USER));
$record = reset($records);
// First check if user has any other input or setup factors active.
$factors = $this->get_affecting_factors();
$total = 0;
foreach ($factors as $factor) {
$total += $factor->get_weight();
// If we have hit 100 total, then we know it is possible to auth with the current setup.
// Gracemode should no longer give points.
if ($total >= 100) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
$starttime = $record->timecreated;
// If no start time is recorded, status is unknown.
if (empty($starttime)) {
return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
} else {
$duration = get_config('factor_grace', 'graceperiod');
if (!empty($duration)) {
if (time() > $starttime + $duration) {
// If gracemode would have given points, but now doesnt,
// Jump out of the loop and force a factor setup.
// We will return once there is a setup, or the user tries to leave.
if (get_config('factor_grace', 'forcesetup') && $redirectable) {
if (empty($SESSION->mfa_gracemode_recursive)) {
// Set a gracemode lock so any further recursive gets fall past any recursive calls.
$SESSION->mfa_gracemode_recursive = true;
$factorurls = \tool_mfa\manager::get_no_redirect_urls();
$cleanurl = new \moodle_url($FULLME);
foreach ($factorurls as $factorurl) {
if ($factorurl->compare($cleanurl)) {
$redirectable = false;
}
}
// We should never redirect if we have already passed.
if ($redirectable && \tool_mfa\manager::get_cumulative_weight() >= 100) {
$redirectable = false;
}
unset($SESSION->mfa_gracemode_recursive);
if ($redirectable) {
redirect(new \moodle_url('/admin/tool/mfa/user_preferences.php'),
get_string('redirectsetup', 'factor_grace'));
}
}
}
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
} else {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
} else {
return \tool_mfa\plugininfo\factor::STATE_UNKNOWN;
}
}
}
/**
* Grace Factor implementation.
* State cannot be set. Return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* Grace Factor implementation.
* Add a notification on the next page.
*
* {@inheritDoc}
*/
public function post_pass_state(): void {
global $USER;
parent::post_pass_state();
// Ensure grace factor passed before displaying notification.
if ($this->get_state() == \tool_mfa\plugininfo\factor::STATE_PASS
&& !\tool_mfa\manager::check_factor_pending($this->name)) {
$url = new \moodle_url('/admin/tool/mfa/user_preferences.php');
$link = \html_writer::link($url, get_string('preferences', 'factor_grace'));
$records = ($this->get_all_user_factors($USER));
$record = reset($records);
$starttime = $record->timecreated;
$timeremaining = ($starttime + get_config('factor_grace', 'graceperiod')) - time();
$time = format_time($timeremaining);
$data = ['url' => $link, 'time' => $time];
$customwarning = get_config('factor_grace', 'customwarning');
if (!empty($customwarning)) {
// Clean text, then swap placeholders for time and the setup link.
$message = preg_replace("/{timeremaining}/", $time, $customwarning);
$message = preg_replace("/{setuplink}/", $url, $message);
$message = clean_text($message, FORMAT_MOODLE);
} else {
$message = get_string('setupfactors', 'factor_grace', $data);
}
\core\notification::error($message);
}
}
/**
* Grace Factor implementation.
* Gracemode should not be a valid combination with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool {
// If this combination has more than 1 factor that has setup or input, not valid.
foreach ($combination as $factor) {
if ($factor->has_setup() || $factor->has_input()) {
return false;
}
}
return true;
}
/**
* Grace Factor implementation.
* Gracemode can change outcome just by waiting, or based on other factors.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
];
}
/**
* Grace factor implementation.
*
* If grace period should redirect at end, make this a no-redirect url.
*
* @return array
*/
public function get_no_redirect_urls(): array {
$redirect = get_config('factor_grace', 'forcesetup');
// First check if user has any other input or setup factors active.
$factors = $this->get_affecting_factors();
$total = 0;
foreach ($factors as $factor) {
$total += $factor->get_weight();
// If we have hit 100 total, then we know it is possible to auth with the current setup.
// The setup URL should no longer be a no-redirect URL. User MUST use existing auth.
if ($total >= 100) {
return [];
}
}
if ($redirect && $this->get_state(false) === \tool_mfa\plugininfo\factor::STATE_NEUTRAL) {
// If the config is enabled, the user should be able to access + setup a factor using these pages.
return [
new \moodle_url('/admin/tool/mfa/user_preferences.php'),
new \moodle_url('/admin/tool/mfa/action.php'),
];
} else {
return [];
}
}
/**
* Returns a list of factor objects that can affect gracemode giving points.
*
* Only factors that a user can setup or manually use can affect whether gracemode gives points.
* The intest is to provide a grace period for users to go in, setup factors, phone numbers, etc.,
* so that they are able to authenticate correctly once the grace period ends.
*
* @return array
*/
public function get_all_affecting_factors(): array {
// Check if user has any other input or setup factors active.
$factors = \tool_mfa\plugininfo\factor::get_factors();
$factors = array_filter($factors, function ($el) {
return $el->has_input() || $el->has_setup();
});
return $factors;
}
/**
* Get the factor list that is currently affecting gracemode. Active and not ignored.
*
* @return array
*/
public function get_affecting_factors(): array {
// We need to filter all active user factors against the affecting factors and ignorelist.
// Map active to names for filtering.
$active = \tool_mfa\plugininfo\factor::get_active_user_factor_types();
$active = array_map(function ($el) {
return $el->name;
}, $active);
$factors = $this->get_all_affecting_factors();
$ignorelist = get_config('factor_grace', 'ignorelist');
$ignorelist = !empty($ignorelist) ? explode(',', $ignorelist) : [];
$factors = array_filter($factors, function ($el) use ($ignorelist, $active) {
return !in_array($el->name, $ignorelist) && in_array($el->name, $active);
});
return $factors;
}
}
@@ -0,0 +1,40 @@
<?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 factor_grace\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_grace
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,78 @@
<?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/>.
/**
* Scheduled task to revoke expired factors
*
* @package factor_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_grace\task;
/**
* Scheduled task to revoke expired gracemode factors
*/
class revoke_expired_factors extends \core\task\scheduled_task {
/**
* Return the task's name as shown in admin screens.
*
* @return string
*/
public function get_name(): string {
return get_string('revokeexpiredfactors', 'factor_grace');
}
/**
* Execute the task.
*
* @return void
*/
public function execute(): void {
mtrace('Starting to revoke expired Grace factors');
$this->revoke_factors();
}
/**
* Revokes all grace factors that have a valid timecreated and are outside the duration.
*
* @return void
*/
private function revoke_factors(): void {
global $DB;
// If config is not set, pull out.
$duration = get_config('factor_grace', 'graceperiod');
if (!$duration) {
mtrace('Gracemode duration is not set. Exiting...');
return;
}
$revoketime = time() - $duration;
// Single query implementation.
$sql = "UPDATE {tool_mfa}
SET revoked = 1,
timemodified = :timemodified
WHERE timecreated < :revoketime
AND factor = :factor";
$DB->execute($sql, ['timemodified' => time(), 'revoketime' => $revoketime, 'factor' => 'grace']);
mtrace('Finished revoking expired Grace factors');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Task scheduler
*
* @package factor_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = [
[
'classname' => 'factor_grace\task\revoke_expired_factors',
'blocking' => 0,
'minute' => 'R',
'hour' => '0',
'day' => '*',
'month' => '*',
'dayofweek' => '*',
],
];
@@ -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/>.
/**
* Language strings.
*
* @package factor_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['info'] = 'Allows login without other factor for a specified period of time.';
$string['pluginname'] = 'Grace period';
$string['preferences'] = 'User preferences';
$string['privacy:metadata'] = 'The Grace period factor plugin does not store any personal data';
$string['redirectsetup'] = 'You must complete setup for Multi-factor authentication before you can proceed.';
$string['revokeexpiredfactors'] = 'Revoke expired grace period factors';
$string['settings:customwarning'] = 'Warning banner content';
$string['settings:customwarning_help'] = 'Add content here to replace the grace warning notification with custom HTML contents. Adding {timeremaining} in text will replace it with the current grace duration for the user, and {setuplink} will replace with the URL of the setup page for the user.';
$string['settings:forcesetup'] = 'Force factor setup';
$string['settings:forcesetup_help'] = 'Forces a user to the preferences page to set up multi-factor authentication when the grace period expires. If unchecked, users will be unable to authenticate when the grace period expires.';
$string['settings:graceperiod'] = 'Grace period';
$string['settings:graceperiod_help'] = 'Period of time when users can access the site without configured and enabled factors.';
$string['settings:ignorelist'] = 'Ignored factors';
$string['settings:ignorelist_help'] = 'Grace period will not give points if there are other factors that users can use to authenticate with multi-factor authentication. Any factors here will not be counted by Grace period when deciding whether to give points. This can allow Grace period to allow authentication if another factor like email, has configuration or system issues.';
$string['setupfactors'] = 'You are currently in the grace period, and may not have enough factors set up to log in once the grace period expires. Go to {$a->url} to check your authentication status and set up more authentication factors. Your grace period expires in {$a->time}.';
$string['summarycondition'] = 'is within grace period';
+60
View File
@@ -0,0 +1,60 @@
<?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/>.
/**
* Settings
*
* @package factor_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_grace/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('grace', get_config('factor_grace', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_grace/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configcheckbox('factor_grace/forcesetup',
new lang_string('settings:forcesetup', 'factor_grace'),
new lang_string('settings:forcesetup_help', 'factor_grace'), 0));
$settings->add(new admin_setting_configduration('factor_grace/graceperiod',
new lang_string('settings:graceperiod', 'factor_grace'),
new lang_string('settings:graceperiod_help', 'factor_grace'), '604800'));
$gracefactor = \tool_mfa\plugininfo\factor::get_factor('grace');
$factors = $gracefactor->get_all_affecting_factors();
$gracefactors = [];
foreach ($factors as $factor) {
$gracefactors[$factor->name] = $factor->get_display_name();
}
$settings->add(new admin_setting_configmultiselect('factor_grace/ignorelist',
new lang_string('settings:ignorelist', 'factor_grace'),
new lang_string('settings:ignorelist_help', 'factor_grace'), [], $gracefactors));
$settings->add(new admin_setting_confightmleditor('factor_grace/customwarning',
new lang_string('settings:customwarning', 'factor_grace'),
new lang_string('settings:customwarning_help', 'factor_grace'), '', PARAM_RAW));
@@ -0,0 +1,64 @@
<?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 factor_grace;
/**
* Tests for grace factor.
*
* @package factor_grace
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Test affecting factors
*
* @covers ::get_affecting_factors
* @return void
*/
public function test_affecting_factors(): void {
$this->resetAfterTest(true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$grace = \tool_mfa\plugininfo\factor::get_factor('grace');
$affecting = $grace->get_affecting_factors();
$this->assertEquals(0, count($affecting));
set_config('enabled', 1, 'factor_totp');
$totpfactor = \tool_mfa\plugininfo\factor::get_factor('totp');
$totpdata = [
'secret' => 'fakekey',
'devicename' => 'fakedevice',
];
$totpfactor->setup_user_factor((object) $totpdata);
// Confirm that MFA is the only affecting factor.
$affecting = $grace->get_affecting_factors();
$this->assertEquals(1, count($affecting));
$totp = reset($affecting);
$this->assertTrue($totp instanceof \factor_totp\factor);
// Now put it in the ignorelist.
set_config('ignorelist', 'totp', 'factor_grace');
// Confirm that MFA is the only affecting factor.
$affecting = $grace->get_affecting_factors();
$this->assertEquals(0, count($affecting));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_grace
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_grace'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,114 @@
<?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 factor_iprange;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* IP Range factor class.
*
* @package factor_iprange
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* IP Range Factor implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* IP Range Factor implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* IP Range Factor implementation.
* Checks a users current IP against allowed and disallowed ranges.
*
* {@inheritDoc}
*/
public function get_state(): string {
$safeips = get_config('factor_iprange', 'safeips');
// TODO: Check for failures here.
if (!empty($safeips)) {
if (remoteip_in_list($safeips)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
}
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
/**
* IP Range Factor implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* IP Range Factor implementation.
* User can influence state prior to login.
* Possible states are either neutral or pass.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
];
}
}
@@ -0,0 +1,40 @@
<?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 factor_iprange\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,33 @@
<?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/>.
/**
* Language strings.
*
* @package factor_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['allowedipsempty'] = 'Nobody will currently pass this factor! You can add your own IP address (<i>{$a->ip}</i>)';
$string['allowedipshasmyip'] = 'Your IP (<i>{$a->ip}</i>) is in the list and you will pass this factor.';
$string['allowedipshasntmyip'] = 'Your IP (<i>{$a->ip}</i>) is not in the list and you will not pass this factor.';
$string['pluginname'] = 'IP range';
$string['privacy:metadata'] = 'The IP range factor plugin does not store any personal data.';
$string['settings:safeips'] = 'Safe IP ranges';
$string['settings:safeips_help'] = 'Enter a list of IP addresses or subnets to be counted as a pass in factor. If empty nobody will pass this factor. {$a->info} {$a->syntax}';
$string['summarycondition'] = 'is on a secured network';
@@ -0,0 +1,61 @@
<?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/>.
/**
* Settings
*
* @package factor_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $OUTPUT;
$enabled = new admin_setting_configcheckbox('factor_iprange/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('iprange', get_config('factor_iprange', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_iprange/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
// Current IP validation against list for description.
$allowedips = get_config('factor_iprange', 'safeips');
if (trim($allowedips) == '') {
$message = 'allowedipsempty';
$type = 'notifyerror';
} else if (remoteip_in_list($allowedips)) {
$message = 'allowedipshasmyip';
$type = 'notifysuccess';
} else {
$message = 'allowedipshasntmyip';
$type = 'notifyerror';
};
$info = $OUTPUT->notification(get_string($message, 'factor_iprange', ['ip' => getremoteaddr()]), $type);
$settings->add(new admin_setting_configiplist('factor_iprange/safeips',
new lang_string('settings:safeips', 'factor_iprange'),
new lang_string('settings:safeips_help', 'factor_iprange',
['info' => $info, 'syntax' => get_string('ipblockersyntax', 'admin')]), '', PARAM_TEXT));
+31
View File
@@ -0,0 +1,31 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_iprange
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_iprange'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,128 @@
<?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 factor_nosetup;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* No setup factor class.
*
* @package factor_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* No Setup Factor implementation.
* Factor is a singleton, can only be one instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* No Setup Factor implementation.
* Factor does not have input.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* No Setup Factor implementation.
* State check is performed here, as there is no form to do it in.
*
* {@inheritDoc}
*/
public function get_state(): string {
// Check if user has any other input or setup factors active.
$factors = \tool_mfa\plugininfo\factor::get_active_user_factor_types();
foreach ($factors as $factor) {
if ($factor->has_input() || $factor->has_setup()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* No setup implementation.
* Copy of get_state, but can take other user..
*
* @param stdClass $user
* @return void
*/
public function possible_states(stdClass $user): array {
// We return Neutral here because to support optional rollouts
// it needs to report neutral or the menu to setup will not display.
return [\tool_mfa\plugininfo\factor::STATE_NEUTRAL];
}
/**
* No Setup Factor implementation.
* The state can never be set. Always return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* No Setup Factor implementation.
* nosetup should not be a valid combination with another factor.
*
* @param array $combination array of factors that make up the combination
* @return bool
*/
public function check_combination(array $combination): bool {
// If this combination has more than 1 factor that has setup or input, not valid.
foreach ($combination as $factor) {
if ($factor->has_setup() || $factor->has_input()) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,40 @@
<?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 factor_nosetup\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Scheduled task to revoke unusable factors that will never pass.
*
* @package factor_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_nosetup\task;
/**
* Scheduled task to add log events into DB table.
*/
class delete_unusable_factors extends \core\task\scheduled_task {
/**
* Return the task's name as shown in admin screens.
*
* @return string
*/
public function get_name(): string {
return get_string('deleteunusablefactors', 'factor_nosetup');
}
/**
* Execute the task.
*
* @return void
*/
public function execute(): void {
mtrace('Starting to revoke unusable Nosetup factors');
$this->revoke_factors();
}
/**
* Revokes all nosetup factors that will now always fail.
*
* @return void
*/
private function revoke_factors(): void {
global $DB;
$factorobject = \tool_mfa\plugininfo\factor::get_factor('nosetup');
// We need to get all nosetup factors, and check that for ones that no longer have a pass state.
$allfactorssql = "SELECT DISTINCT tm.userid
FROM {tool_mfa} tm
JOIN {user} u ON u.id = tm.userid
WHERE tm.factor = :factor
AND u.suspended = 0
AND u.deleted = 0
AND (
SELECT COUNT(id) as count
FROM {tool_mfa}
WHERE userid = tm.userid
AND factor <> :notfactor
) > 0";
$useridrecordset = $DB->get_recordset_sql($allfactorssql, ['factor' => 'nosetup', 'notfactor' => 'nosetup']);
foreach ($useridrecordset as $userid) {
// If pass state is no longer possible, add delete user factor.
$user = \core_user::get_user($userid->userid);
if (!in_array(\tool_mfa\plugininfo\factor::STATE_PASS, $factorobject->possible_states($user))) {
$factorobject->delete_factor_for_user($user);
}
}
$useridrecordset->close();
mtrace('Finished revoking unusable Nosetup factors');
}
}
@@ -0,0 +1,38 @@
<?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/>.
/**
* Task scheduler
*
* @package factor_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = [
[
'classname' => 'factor_nosetup\task\delete_unusable_factors',
'blocking' => 0,
'minute' => 'R',
'hour' => '0',
'day' => '*',
'month' => '*',
'dayofweek' => '*',
],
];
@@ -0,0 +1,30 @@
<?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/>.
/**
* Language strings.
*
* @package factor_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['deleteunusablefactors'] = 'Delete unusable Nosetup factors';
$string['info'] = 'This factor passes if the user has no other factors set up.';
$string['pluginname'] = 'No other factors';
$string['privacy:metadata'] = 'The No other factors plugin does not store any personal data';
$string['summarycondition'] = 'has no other factors set up';
@@ -0,0 +1,38 @@
<?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/>.
/**
* Settings
*
* @package factor_nosetup
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_nosetup/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('nosetup', get_config('factor_nosetup', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_nosetup/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_nosetup
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_nosetup'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,182 @@
<?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 factor_role;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* Role factor class.
*
* @package factor_role
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* Role implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Role implementation.
* Factor has no input
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Role implementation.
* Checks whether the user has selected roles in any context.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$rolestring = get_config('factor_role', 'roles');
// Nothing selected, everyone passes.
if (empty($rolestring)) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
$selected = explode(',', $rolestring);
$syscon = \context_system::instance();
$specials = get_user_roles_with_special($syscon, $USER->id);
// Transform the special roles to the matching format.
$specials = array_map(function ($el) {
return $el->roleid;
}, $specials);
foreach ($selected as $id) {
if ($id === 'admin') {
if (is_siteadmin()) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
} else {
if (user_has_role_assignment($USER->id, $id)) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
// Some system default roles do not have an explicit binding. eg Authenticated user.
if (in_array((int) $id, $specials)) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
}
}
// If we got here, no roles matched, allow access.
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
/**
* Role implementation.
* Cannot set state, return true.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state(string $state): bool {
return true;
}
/**
* Role implementation.
* User can not influence. Result is whatever current state is.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [$this->get_state()];
}
/**
* Role implementation
* Formats the role list nicely.
*
* {@inheritDoc}
*/
public function get_summary_condition(): string {
$selectedroles = get_config('factor_role', 'roles');
if (empty($selectedroles)) {
return get_string('summarycondition', 'factor_role', get_string('none'));
}
$selectedroles = $this->get_roles(explode(',', $selectedroles));
if (empty($selectedroles)) {
return get_string('summarycondition', 'factor_role', get_string('none'));
}
return get_string('summarycondition', 'factor_role', implode(', ', $selectedroles));
}
/**
* Get roles information by given ids.
*
* @param array $selectedroles List of role ids.
* @return array
*/
public function get_roles(array $selectedroles): array {
global $DB;
$roles = [];
// Checks for admin role and gets its role name.
if (in_array('admin', $selectedroles)) {
$roles[] = get_string('administrator');
}
$integerroles = array_map('intval', $selectedroles);
// Gets role name for all non admin roles.
if (!empty($integerroles)) {
[$insql, $inparams] = $DB->get_in_or_equal($integerroles);
$otherroles = $DB->get_records_select('role', 'id ' . $insql, $inparams);
$otherrolenames = role_fix_names($otherroles, null, ROLENAME_ALIAS, true);
$roles = array_merge($roles, $otherrolenames);
}
return $roles;
}
}
@@ -0,0 +1,40 @@
<?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 factor_role\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_role
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,30 @@
<?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/>.
/**
* Language strings.
*
* @package factor_role
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Role';
$string['privacy:metadata'] = 'The Role factor plugin does not store any personal data.';
$string['settings:roles'] = 'Non-passing roles';
$string['settings:roles_help'] = 'Select the roles that will not pass this factor. This allows you to force these roles to use other factors to authenticate.';
$string['summarycondition'] = 'does NOT have any of the following roles assigned in any context: {$a}';
+48
View File
@@ -0,0 +1,48 @@
<?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/>.
/**
* Settings
*
* @package factor_role
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_role/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('role', get_config('factor_role', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_role/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$choices = ['admin' => get_string('administrator')];
$roles = get_all_roles();
foreach ($roles as $role) {
$choices[$role->id] = role_get_name($role);
}
$settings->add(new admin_setting_configmultiselect('factor_role/roles',
new lang_string('settings:roles', 'factor_role'),
new lang_string('settings:roles_help', 'factor_role'), ['admin'], $choices));
@@ -0,0 +1,126 @@
<?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 factor_role;
/**
* Tests for role factor.
*
* @covers \factor_role\factor
* @package factor_role
* @copyright 2023 Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Tests getting the summary condition
*
* @covers ::get_summary_condition
* @covers ::get_roles
*/
public function test_get_summary_condition(): void {
global $DB;
$this->resetAfterTest();
$managerrole = $DB->get_record('role', ['shortname' => 'manager']);
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$adminrolename = get_string('administrator');
$managerrolename = role_get_name($managerrole);
$teacherrolename = role_get_name($teacherrole);
$studentrolename = role_get_name($studentrole);
set_config('enabled', 1, 'factor_role');
$rolefactor = \tool_mfa\plugininfo\factor::get_factor('role');
// Admin is disabled by default in this factor.
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertContains($adminrolename, $selectedroles);
$this->assertNotContains($managerrolename, $selectedroles);
$this->assertNotContains($teacherrolename, $selectedroles);
$this->assertNotContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for managers.
set_config('roles', $managerrole->id, 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertNotContains($adminrolename, $selectedroles);
$this->assertContains($managerrolename, $selectedroles);
$this->assertNotContains($teacherrolename, $selectedroles);
$this->assertNotContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for teachers.
set_config('roles', $teacherrole->id, 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertNotContains($adminrolename, $selectedroles);
$this->assertNotContains($managerrolename, $selectedroles);
$this->assertContains($teacherrolename, $selectedroles);
$this->assertNotContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for students.
set_config('roles', $studentrole->id, 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertNotContains($adminrolename, $selectedroles);
$this->assertNotContains($managerrolename, $selectedroles);
$this->assertNotContains($teacherrolename, $selectedroles);
$this->assertContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Disabled role factor for admins, managers, teachers and students.
set_config('roles', "admin,$managerrole->id,$teacherrole->id,$studentrole->id", 'factor_role');
$selectedroles = get_config('factor_role', 'roles');
$selectedroles = $rolefactor->get_roles(explode(',', $selectedroles));
$this->assertContains($adminrolename, $selectedroles);
$this->assertContains($managerrolename, $selectedroles);
$this->assertContains($teacherrolename, $selectedroles);
$this->assertContains($studentrolename, $selectedroles);
$this->assertStringContainsString(
implode(', ', $selectedroles),
$rolefactor->get_summary_condition()
);
// Enable all roles.
unset_config('roles', 'factor_role');
$this->assertEquals(
get_string('summarycondition', 'factor_role', get_string('none')),
$rolefactor->get_summary_condition()
);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_role
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_role'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,62 @@
<?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 factor_sms\event;
/**
* Event for a sent SMS
*
* @package factor_sms
* @author Alex Morris <alex.morris@catalyst.net.nz>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sms_sent extends \core\event\base {
/**
* Init sms sent event
*/
protected function init() {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns non-localised event description with id's for admin use only.
*
* @return string
*/
public function get_description(): string {
$content = [
'userid' => $this->other['userid'],
'debuginfo' => is_array($this->other['debug']) ? json_encode($this->other['debug']) : $this->other['debug'],
];
return get_string('event:smssentdescription', 'factor_sms', $content);
}
/**
* Returns localised general event name.
*
* Override in subclass, we can not make it static and abstract at the same time.
*
* @return string
*/
public static function get_name(): string {
return get_string('event:smssent', 'factor_sms');
}
}
@@ -0,0 +1,460 @@
<?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 factor_sms;
use moodle_url;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
/**
* SMS Factor implementation.
*
* @package factor_sms
* @subpackage 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 factor extends object_factor_base {
/** @var string Factor icon */
protected $icon = 'fa-commenting-o';
/**
* Defines login form definition page for SMS Factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
$mform->addElement(new \tool_mfa\local\form\verification_field());
$mform->setType('verificationcode', PARAM_ALPHANUM);
return $mform;
}
/**
* Defines login form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return \MoodleQuickForm $mform
*/
public function login_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
$this->generate_and_sms_code();
// Disable the form check prompt.
$mform->disable_form_change_checker();
return $mform;
}
/**
* Implements login form validation for SMS Factor.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
$return = [];
if (!$this->check_verification_code($data['verificationcode'])) {
$return['verificationcode'] = get_string('error:wrongverification', 'factor_sms');
}
return $return;
}
/**
* Gets the string for setup button on preferences page.
*
* @return string
*/
public function get_setup_string(): string {
return get_string('setupfactorbutton', 'factor_sms');
}
/**
* Gets the string for manage button on preferences page.
*
* @return string
*/
public function get_manage_string(): string {
return get_string('managefactorbutton', 'factor_sms');
}
/**
* Defines setup_factor form definition page for SMS Factor.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
global $OUTPUT, $USER, $DB;
if (!empty(
$phonenumber = $DB->get_field('tool_mfa', 'label', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0])
)) {
redirect(
new \moodle_url('/admin/tool/mfa/user_preferences.php'),
get_string('factorsetup', 'tool_mfa', $phonenumber),
null,
\core\output\notification::NOTIFY_SUCCESS);
}
$mform->addElement('html', $OUTPUT->heading(get_string('setupfactor', 'factor_sms'), 2));
if (empty($this->get_phonenumber())) {
$mform->addElement('hidden', 'verificationcode', 0);
$mform->setType('verificationcode', PARAM_ALPHANUM);
// Add field for phone number setup.
$mform->addElement('text', 'phonenumber', get_string('addnumber', 'factor_sms'),
[
'autocomplete' => 'tel',
'inputmode' => 'tel',
]);
$mform->setType('phonenumber', PARAM_TEXT);
// HTML to display a message about the phone number.
$message = \html_writer::tag('div', '', ['class' => 'col-md-3']);
$message .= \html_writer::tag(
'div', \html_writer::tag('p', get_string('phonehelp', 'factor_sms')), ['class' => 'col-md-9']);
$mform->addElement('html', \html_writer::tag('div', $message, ['class' => 'row']));
}
return $mform;
}
/**
* Defines setup_factor form definition page after form data has been set.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
global $OUTPUT;
$phonenumber = $this->get_phonenumber();
if (empty($phonenumber)) {
return $mform;
}
$duration = get_config('factor_sms', 'duration');
$code = $this->secretmanager->create_secret($duration, true);
if (!empty($code)) {
$this->sms_verification_code($code, $phonenumber);
}
$message = get_string('logindesc', 'factor_sms', '<b>' . $phonenumber . '</b><br/>');
$message .= get_string('editphonenumberinfo', 'factor_sms');
$mform->addElement('html', \html_writer::tag('p', $OUTPUT->notification($message, 'success')));
$mform->addElement(new \tool_mfa\local\form\verification_field());
$mform->setType('verificationcode', PARAM_ALPHANUM);
$editphonenumber = \html_writer::link(
new \moodle_url('/admin/tool/mfa/factor/sms/editphonenumber.php', ['sesskey' => sesskey()]),
get_string('editphonenumber', 'factor_sms'),
['class' => 'btn btn-secondary', 'type' => 'button']);
$mform->addElement('html', \html_writer::tag('div', $editphonenumber, ['class' => 'float-sm-left col-md-4']));
// Disable the form check prompt.
$mform->disable_form_change_checker();
return $mform;
}
/**
* Returns the phone number from the current session or from the user profile data.
* @return string|null
*/
private function get_phonenumber(): ?string {
global $SESSION, $USER, $DB;
if (!empty($SESSION->tool_mfa_sms_number)) {
return $SESSION->tool_mfa_sms_number;
}
$phonenumber = $DB->get_field('tool_mfa', 'label', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0]);
if (!empty($phonenumber)) {
return $phonenumber;
}
return null;
}
/**
* 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 {
$errors = [];
// Phone number validation.
if (!empty($data["phonenumber"]) && empty(helper::is_valid_phonenumber($data["phonenumber"]))) {
$errors['phonenumber'] = get_string('error:wrongphonenumber', 'factor_sms');
} else if (!empty($this->get_phonenumber())) {
// Code validation.
if (empty($data["verificationcode"])) {
$errors['verificationcode'] = get_string('error:emptyverification', 'factor_sms');
} else if ($this->secretmanager->validate_secret($data['verificationcode']) !== $this->secretmanager::VALID) {
$errors['verificationcode'] = get_string('error:wrongverification', 'factor_sms');
}
}
return $errors;
}
/**
* Reset values of the session data of the given factor.
*
* @param int $factorid
* @return void
*/
public function setup_factor_form_is_cancelled(int $factorid): void {
global $SESSION;
if (!empty($SESSION->tool_mfa_sms_number)) {
unset($SESSION->tool_mfa_sms_number);
}
// Clean temp secrets code.
$secretmanager = new \tool_mfa\local\secret_manager('sms');
$secretmanager->cleanup_temp_secrets();
}
/**
* Setup submit button string in given factor
*
* @return string|null
*/
public function setup_factor_form_submit_button_string(): ?string {
global $SESSION;
if (!empty($SESSION->tool_mfa_sms_number)) {
return get_string('setupsubmitcode', 'factor_sms');
}
return get_string('setupsubmitphone', 'factor_sms');
}
/**
* Adds an instance of the factor for a user, from form data.
*
* @param stdClass $data
* @return stdClass|null the factor record, or null.
*/
public function setup_user_factor(stdClass $data): ?stdClass {
global $DB, $SESSION, $USER;
// Handle phone number submission.
if (empty($SESSION->tool_mfa_sms_number)) {
$SESSION->tool_mfa_sms_number = !empty($data->phonenumber) ? $data->phonenumber : '';
$addurl = new \moodle_url('/admin/tool/mfa/action.php', [
'action' => 'setup',
'factor' => 'sms',
]);
redirect($addurl);
}
// If the user somehow gets here through form resubmission.
// We dont want two phones active.
if ($DB->record_exists('tool_mfa', ['userid' => $USER->id, 'factor' => $this->name, 'revoked' => 0])) {
return null;
}
$time = time();
$label = $this->get_phonenumber();
$row = new \stdClass();
$row->userid = $USER->id;
$row->factor = $this->name;
$row->secret = '';
$row->label = $label;
$row->timecreated = $time;
$row->createdfromip = $USER->lastip;
$row->timemodified = $time;
$row->lastverified = $time;
$row->revoked = 0;
$id = $DB->insert_record('tool_mfa', $row);
$record = $DB->get_record('tool_mfa', ['id' => $id]);
$this->create_event_after_factor_setup($USER);
// Remove session phone number.
unset($SESSION->tool_mfa_sms_number);
return $record;
}
/**
* Returns an array of all user factors of given type.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$sql = 'SELECT *
FROM {tool_mfa}
WHERE userid = ?
AND factor = ?
AND label IS NOT NULL
AND revoked = 0';
return $DB->get_records_sql($sql, [$user->id, $this->name]);
}
/**
* Returns the information about factor availability.
*
* @return bool
*/
public function is_enabled(): bool {
if (empty(get_config('factor_sms', 'gateway'))) {
return false;
}
$class = '\factor_sms\local\smsgateway\\' . get_config('factor_sms', 'gateway');
if (!call_user_func($class . '::is_gateway_enabled')) {
return false;
}
return parent::is_enabled();
}
/**
* Decides if a factor requires input from the user to verify.
*
* @return bool
*/
public function has_input(): bool {
return true;
}
/**
* Decides if factor needs to be setup by user and has setup_form.
*
* @return bool
*/
public function has_setup(): bool {
return true;
}
/**
* Decides if the setup buttons should be shown on the preferences page.
*
* @return bool
*/
public function show_setup_buttons(): bool {
return true;
}
/**
* 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 {
return true;
}
/**
* Generates and sms' the code for login to the user, stores codes in DB.
*
* @return int|null the instance ID being used.
*/
private function generate_and_sms_code(): ?int {
global $DB, $USER;
$duration = get_config('factor_sms', 'duration');
$instance = $DB->get_record('tool_mfa', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0]);
if (empty($instance)) {
return null;
}
$secret = $this->secretmanager->create_secret($duration, false);
// There is a new code that needs to be sent.
if (!empty($secret)) {
// Grab the singleton SMS record.
$this->sms_verification_code($secret, $instance->label);
}
return $instance->id;
}
/**
* This function sends an SMS code to the user based on the phonenumber provided.
*
* @param int $secret the secret to send.
* @param string|null $phonenumber the phonenumber to send the verification code to.
* @return void
*/
private function sms_verification_code(int $secret, ?string $phonenumber): void {
global $CFG, $SITE;
// Here we should get the information, then construct the message.
$url = new moodle_url($CFG->wwwroot);
$content = [
'fullname' => $SITE->fullname,
'url' => $url->get_host(),
'code' => $secret,
];
$message = get_string('smsstring', 'factor_sms', $content);
$class = '\factor_sms\local\smsgateway\\' . get_config('factor_sms', 'gateway');
$gateway = new $class();
$gateway->send_sms_message($message, $phonenumber);
}
/**
* Verifies entered code against stored DB record.
*
* @param string $enteredcode
* @return bool
*/
private function check_verification_code(string $enteredcode): bool {
return ($this->secretmanager->validate_secret($enteredcode) === \tool_mfa\local\secret_manager::VALID) ? true : false;
}
/**
* Returns all possible states for a user.
*
* @param \stdClass $user
*/
public function possible_states(\stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
\tool_mfa\plugininfo\factor::STATE_FAIL,
\tool_mfa\plugininfo\factor::STATE_UNKNOWN,
];
}
/**
* 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 {
$phonenumber = $this->get_phonenumber();
if (empty($phonenumber)) {
return get_string('errorsmssent', 'factor_sms');
} else {
return get_string('logindesc', 'factor_' . $this->name, $phonenumber);
}
}
}
@@ -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 factor_sms;
/**
* Helper class for shared sms gateway functions
*
* @package factor_sms
* @author Alex Morris <alex.morris@catalyst.net.nz>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* This function internationalises a number to E.164 standard.
* https://46elks.com/kb/e164
*
* @param string $phonenumber the phone number to format.
* @return string the formatted phone number.
*/
public static function format_number(string $phonenumber): string {
// Remove all whitespace, dashes and brackets.
$phonenumber = preg_replace('/[ \(\)-]/', '', $phonenumber);
// Number is already in international format. Do nothing.
if (str_starts_with ($phonenumber, '+')) {
return $phonenumber;
}
// Strip leading 0 if found.
if (str_starts_with ($phonenumber, '0')) {
$phonenumber = substr($phonenumber, 1);
}
// Prepend country code.
$countrycode = get_config('factor_sms', 'countrycode');
$phonenumber = !empty($countrycode) ? '+' . $countrycode . $phonenumber : $phonenumber;
return $phonenumber;
}
/**
* Validate phone number with E.164 format. https://en.wikipedia.org/wiki/E.164
*
* @param string $phonenumber from the given user input
* @return bool
*/
public static function is_valid_phonenumber(string $phonenumber): bool {
$phonenumber = self::format_number($phonenumber);
return (preg_match("/^\+[1-9]\d{1,14}$/", $phonenumber)) ? true : false;
}
}
@@ -0,0 +1,153 @@
<?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 factor_sms\local\smsgateway;
use core\aws\admin_settings_aws_region;
use core\aws\aws_helper;
use factor_sms\event\sms_sent;
/**
* AWS SNS SMS Gateway class
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class aws_sns implements gateway_interface {
/**
* Create an instance of this class.
*/
public function __construct() {
global $CFG;
require_once($CFG->libdir . '/aws-sdk/src/functions.php');
require_once($CFG->libdir . '/guzzlehttp/guzzle/src/functions_include.php');
require_once($CFG->libdir . '/guzzlehttp/promises/src/functions_include.php');
}
/**
* Sends a message using the AWS SNS API
*
* @param string $messagecontent the content to send in the SMS message.
* @param string $phonenumber the destination for the message.
* @return bool true on message send success
*/
public function send_sms_message(string $messagecontent, string $phonenumber): bool {
global $SITE, $USER;
$config = get_config('factor_sms');
// Setup client params and instantiate client.
$params = [
'version' => 'latest',
'region' => $config->api_region,
'http' => ['proxy' => aws_helper::get_proxy_string()],
];
if (!$config->usecredchain) {
$params['credentials'] = [
'key' => $config->api_key,
'secret' => $config->api_secret,
];
}
$client = new \Aws\Sns\SnsClient($params);
// Transform the phone number to international standard.
$phonenumber = \factor_sms\helper::format_number($phonenumber);
// Setup the sender information.
$senderid = $SITE->shortname;
// Remove spaces and non-alphanumeric characters from ID.
$senderid = preg_replace("/[^A-Za-z0-9]/", '', trim($senderid));
// We have to truncate the senderID to 11 chars.
$senderid = substr($senderid, 0, 11);
if (defined('BEHAT_SITE_RUNNING')) {
// Fake SMS sending in behat.
return true;
}
try {
// These messages need to be transactional.
$client->SetSMSAttributes([
'attributes' => [
'DefaultSMSType' => 'Transactional',
'DefaultSenderID' => $senderid,
],
]);
// Actually send the message.
$result = $client->publish([
'Message' => $messagecontent,
'PhoneNumber' => $phonenumber,
]);
$data = [
'relateduserid' => null,
'context' => \context_user::instance($USER->id),
'other' => [
'userid' => $USER->id,
'debug' => [
'messageid' => $result->get('MessageId'),
],
],
];
$event = sms_sent::create($data);
$event->trigger();
return true;
} catch (\Aws\Exception\AwsException $e) {
throw new \moodle_exception('errorawsconection', 'factor_sms', '', $e->getAwsErrorMessage());
}
}
/**
* Add gateway specific settings to the SMS factor settings page.
*
* @param \admin_settingpage $settings
* @return void
*/
public static function add_settings(\admin_settingpage $settings): void {
$settings->add(new \admin_setting_configcheckbox('factor_sms/usecredchain',
get_string('settings:aws:usecredchain', 'factor_sms'), '', 0));
if (!get_config('factor_sms', 'usecredchain')) {
// AWS Settings.
$settings->add(new \admin_setting_configtext('factor_sms/api_key',
get_string('settings:aws:key', 'factor_sms'),
get_string('settings:aws:key_help', 'factor_sms'), ''));
$settings->add(new \admin_setting_configpasswordunmask('factor_sms/api_secret',
get_string('settings:aws:secret', 'factor_sms'),
get_string('settings:aws:secret_help', 'factor_sms'), ''));
}
$settings->add(new admin_settings_aws_region('factor_sms/api_region',
get_string('settings:aws:region', 'factor_sms'),
get_string('settings:aws:region_help', 'factor_sms'),
'ap-southeast-2'));
}
/**
* Returns whether or not the gateway is enabled
*
* @return bool
*/
public static function is_gateway_enabled(): bool {
return true;
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* SMS Gateway interface
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace factor_sms\local\smsgateway;
interface gateway_interface {
/**
* Sends an SMS message
*
* @param string $messagecontent the content to send in the SMS message.
* @param string $phonenumber the destination for the message.
* @return bool true on message send success
*/
public function send_sms_message(string $messagecontent, string $phonenumber): bool;
/**
* Add gateway specific settings to the SMS factor settings page.
*
* @param \admin_settingpage $settings
* @return void
*/
public static function add_settings(\admin_settingpage $settings): void;
/**
* Returns whether or not the gateway is enabled
*
* @return bool
*/
public static function is_gateway_enabled(): bool;
}
@@ -0,0 +1,40 @@
<?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 factor_sms\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,44 @@
<?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/>.
/**
* Edit phonenumber redirect
*
* @package factor_sms
* @copyright 2023 Raquel Ortega <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../config.php');
require_login(null, false);
if (isguestuser()) {
throw new require_login_exception('error:isguestuser', 'tool_mfa');
}
$sesskey = optional_param('sesskey', false, PARAM_TEXT);
require_sesskey();
// Remove session phone number.
unset($SESSION->tool_mfa_sms_number);
// Clean temp secrets code.
$secretmanager = new \tool_mfa\local\secret_manager('sms');
$secretmanager->cleanup_temp_secrets();
redirect(new \moodle_url('/admin/tool/mfa/action.php', [
'action' => 'setup',
'factor' => 'sms',
]));
@@ -0,0 +1,75 @@
<?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/>.
/**
* Language strings.
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['action:manage'] = 'Manage mobile phone number';
$string['action:revoke'] = 'Remove mobile phone number';
$string['addnumber'] = 'Mobile number';
$string['clientnotfound'] = 'AWS service client not found. Client must be fully qualified classname e.g. \Aws\S3\S3Client.';
$string['editphonenumber'] = 'Edit phone number';
$string['editphonenumberinfo'] = 'If you didn\'t receive the code or entered the wrong number, please edit the number and try again.';
$string['errorawsconection'] = 'Error connecting to AWS server: {$a}';
$string['errorsmssent'] = 'Error sending a SMS message containing your verification code.';
$string['error:emptyverification'] = 'Empty code. Try again.';
$string['error:wrongphonenumber'] = 'The phone number you provided is not in a valid format.';
$string['error:wrongverification'] = 'Wrong code. Try again.';
$string['event:smssent'] = 'SMS message sent.';
$string['event:smssentdescription'] = 'The user with ID {$a->userid} was sent a verification code via SMS. Information: {$a->debuginfo}';
$string['info'] = 'Have a verification code sent to the mobile number you choose.';
$string['logindesc'] = 'SMS message containing a 6-digit code sent to mobile number {$a}';
$string['loginoption'] = 'Have a code sent to your mobile phone';
$string['loginskip'] = "I didn't receive a code";
$string['loginsubmit'] = 'Continue';
$string['managefactor'] = 'Manage SMS';
$string['managefactorbutton'] = 'Manage';
$string['manageinfo'] = 'You are using \'{$a}\' to authenticate.';
$string['logintitle'] = 'Enter the verification code sent to your mobile';
$string['phonehelp'] = 'Enter your mobile number (including country code) to receive a verification code.';
$string['pluginname'] = 'SMS mobile phone';
$string['privacy:metadata'] = 'The SMS mobile phone factor plugin does not store any personal data.';
$string['revokefactorconfirmation'] = 'Remove \'{$a}\' SMS?';
$string['settings:aws'] = 'AWS SNS';
$string['settings:aws:key'] = 'Key';
$string['settings:aws:key_help'] = 'Amazon API key credential.';
$string['settings:aws:region'] = 'Region';
$string['settings:aws:region_help'] = 'Amazon API gateway region.';
$string['settings:aws:secret'] = 'Secret';
$string['settings:aws:secret_help'] = 'Amazon API secret credential.';
$string['settings:aws:usecredchain'] = 'Find AWS credentials using the default credential provider chain';
$string['settings:countrycode'] = 'Country number code';
$string['settings:countrycode_help'] = 'The calling code without the leading + as a default if users do not enter an international number with a + prefix.
See this link for a list of calling codes: {$a}';
$string['settings:duration'] = 'Validity duration';
$string['settings:duration_help'] = 'The period of time that the code is valid.';
$string['settings:gateway'] = 'SMS gateway';
$string['settings:gateway_help'] = 'The SMS provider you wish to send messages via';
$string['setupfactor'] = 'Set up SMS';
$string['setupfactorbutton'] = 'Set up';
$string['setupsubmitcode'] = 'Save';
$string['setupsubmitphone'] = 'Send code';
$string['smsstring'] = '{$a->code} is your {$a->fullname} one-time security code.
@{$a->url} #{$a->code}';
$string['summarycondition'] = 'Using an SMS one-time security code';
+66
View File
@@ -0,0 +1,66 @@
<?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/>.
/**
* Settings
*
* @package factor_sms
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG, $OUTPUT;
$enabled = new admin_setting_configcheckbox('factor_sms/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('sms', get_config('factor_sms', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_sms/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configduration('factor_sms/duration',
get_string('settings:duration', 'tool_mfa'),
get_string('settings:duration_help', 'tool_mfa'), 30 * MINSECS, MINSECS));
$codeslink = 'https://en.wikipedia.org/wiki/List_of_country_calling_codes';
$link = \html_writer::link($codeslink, $codeslink);
$settings->add(new admin_setting_configtext('factor_sms/countrycode',
get_string('settings:countrycode', 'factor_sms'),
get_string('settings:countrycode_help', 'factor_sms', $link), '', PARAM_INT));
$gateways = [
'aws_sns' => get_string('settings:aws', 'factor_sms'),
];
$settings->add(new admin_setting_configselect('factor_sms/gateway',
get_string('settings:gateway', 'factor_sms'),
get_string('settings:gateway_help', 'factor_sms'),
'aws_sns', $gateways));
if (empty(get_config('factor_sms', 'gateway'))) {
return;
}
$class = '\factor_sms\local\smsgateway\\' . get_config('factor_sms', 'gateway');
call_user_func($class . '::add_settings', $settings);
@@ -0,0 +1,55 @@
<?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/>.
/**
* Behat custom steps and configuration for factor_sms.
*
* @package factor_sms
* @category test
* @copyright 2023 <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../../../../../lib/behat/behat_field_manager.php');
/**
* Behat custom steps and configuration for factor_sms.
*
* @package factor_sms
* @category test
* @copyright 2023 <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_factor_sms extends behat_base {
/**
* Sets the given field with a valid code created in tool_mfa_secrets table
*
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" with valid code$/
*
* @param string $field
*/
public function i_set_the_field_with_valid_code(string $field): void {
global $DB, $USER;
$record = $DB->get_record('tool_mfa_secrets',
['userid' => $USER->id, 'revoked' => '0']
);
$field = behat_field_manager::get_form_field_from_label($field, $this);
$field->set_value($record->secret);
}
}
@@ -0,0 +1,47 @@
@tool @tool_mfa @factor_sms
Feature: Login user with sms authentication factor
In order to login using SMS factor authentication
As an user
I need to be able to login
Background:
Given I log in as "admin"
And the following config values are set as admin:
| enabled | 1 | tool_mfa |
| lockout | 3 | tool_mfa |
And the following config values are set as admin:
| enabled | 1 | factor_sms |
# Set up user SMS factor in user preferences.
When I follow "Preferences" in the user menu
And I click on "Multi-factor authentication preferences" "link"
And I click on "Set up" "button"
And I set the field "Mobile number" to "+34649709233"
And I press "Send code"
And I set the field "Enter code" with valid code
Then I press "Save"
Scenario: Login user successfully with sms verification
Given I log out
And I log in as "admin"
And I should see "2-step verification"
And I should see "Enter code"
When I set the field "Enter code" with valid code
And I click on "Continue" "button"
Then I am logged in as "admin"
Scenario: Wrong code number end of possible attempts
Given I log out
And I log in as "admin"
And I should see "2-step verification"
And I should see "Enter code"
When I set the field "Enter code" to "555556"
And I click on "Continue" "button"
And I should see "Wrong code."
And I should see "You have 2 attempts left."
And I set the field "Enter code" to "555553"
And I click on "Continue" "button"
And I should see "Wrong code."
And I should see "1 attempts left."
And I set the field "Enter code" to "555553"
And I click on "Continue" "button"
Then I should see "Unable to authenticate"
@@ -0,0 +1,49 @@
@tool @tool_mfa @factor_sms
Feature: Set up SMS factor in user preferences
In order check the SMS factor verification
As an admin
I want to setup and enable the SMS factor for the current user
Background:
Given I log in as "admin"
And the following config values are set as admin:
| enabled | 1 | tool_mfa |
And the following config values are set as admin:
| enabled | 1 | factor_sms |
When I follow "Preferences" in the user menu
And I click on "Multi-factor authentication preferences" "link"
And I click on "Set up" "button"
Scenario: Phone number setup form validation
Given I set the field "Mobile number" to "++5555sss"
And I press "Send code"
And I should see "The phone number you provided is not in a valid format."
And I set the field "Mobile number" to "0123456789"
And I press "Send code"
And I should see "The phone number you provided is not in a valid format."
And I set the field "Mobile number" to "786-307-3615"
And I press "Send code"
And I should see "The phone number you provided is not in a valid format."
When I set the field "Mobile number" to "649709233"
And I press "Send code"
Then I should see "The phone number you provided is not in a valid format."
Scenario: Edit phone number
Given I set the field "Mobile number" to "+34649709233"
And I press "Send code"
And I click on "Edit phone number" "link"
And I should see "Mobile number"
When I set the field "Mobile number" to "+34649709232"
And I press "Send code"
Then I should see "Enter code"
Scenario: Code setup form validation
Given I set the field "Mobile number" to "+34649709233"
And I press "Send code"
And I should see "Enter code"
When I set the field "Enter code" to "555556"
And I click on "Save" "button"
And I should see "Wrong code. Try again"
And I set the field "Enter code" to "ddddd5"
And I click on "Save" "button"
Then I should see "Wrong code. Try again"
@@ -0,0 +1,165 @@
<?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 factor_sms;
/**
* Tests for sms factor.
*
* @covers \factor_sms\factor
* @package factor_sms
* @copyright 2023 Raquel Ortega <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Data provider for test_format_number().
*
* @return array of different country codes and phone numbers.
*/
public function format_number_provider(): array {
return [
'Phone number with local format' => [
'phonenumber' => '0123456789',
'expected' => '+34123456789',
'countrycode' => '34',
],
'Phone number without international format' => [
'phonenumber' => '123456789',
'expected' => '+34123456789',
'countrycode' => '34',
],
'Phone number with international format' => [
'phonenumber' => '+39123456789',
'expected' => '+39123456789',
],
'Phone number with spaces using international format' => [
'phonenumber' => '+34 123 456 789',
'expected' => '+34123456789',
],
'Phone number with spaces using local format with country code' => [
'phonenumber' => '0 123 456 789',
'expected' => '+34123456789',
'countrycode' => '34',
],
'Phone number with spaces using local format without country code' => [
'phonenumber' => '0 123 456 789',
'expected' => '123456789',
],
];
}
/**
* Test format number with different phones and different country codes
* @covers \factor_sms\helper::format_number
* @dataProvider format_number_provider
*
* @param string $phonenumber Phone number.
* @param string $expected Expected value.
* @param string|null $countrycode Country code.
*/
public function test_format_number(string $phonenumber, string $expected, ?string $countrycode = null): void {
$this->resetAfterTest(true);
set_config('countrycode', $countrycode, 'factor_sms');
$this->assertEquals($expected, \factor_sms\helper::format_number($phonenumber));
}
/**
* Data provider for test_is_valid__phonenumber().
*
* @return array with different phone numebr tests
*/
public function is_valid_phonenumber_provider(): array {
return [
['+919367788755', true],
['8989829304', false],
['+16308520397', true],
['786-307-3615', false],
['+14155552671', true],
['+551155256325', true],
['649709233', false],
['+34649709233', true],
['+aaasss', false],
];
}
/**
* Test is valid phone number in E.164 format (https://en.wikipedia.org/wiki/E.164)
* @covers \factor_sms\helper::is_valid_phonenumber
* @dataProvider is_valid_phonenumber_provider
*
* @param string $phonenumber
* @param bool $valid True if the given phone number is valid, false if is invalid
*/
public function test_is_valid_phonenumber(string $phonenumber, bool $valid): void {
$this->resetAfterTest(true);
if ($valid) {
$this->assertTrue(\factor_sms\helper::is_valid_phonenumber($phonenumber));
} else {
$this->assertFalse(\factor_sms\helper::is_valid_phonenumber($phonenumber));
}
}
/**
* Test set up user factor and verification code with a random phone number
* @covers ::setup_user_factor
* @covers ::check_verification_code
* @covers ::revoke_user_factor
*/
public function test_check_verification_code(): void {
global $SESSION;
$this->resetAfterTest(true);
// Create and login a user and set up the phone number.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Generate a fake phone number and save it in session.
$phonenumber = '+34' . (string)random_int(100000000, 999999999);
$SESSION->tool_mfa_sms_number = $phonenumber;
$smsfactor = \tool_mfa\plugininfo\factor::get_factor('sms');
$rc = new \ReflectionClass($smsfactor::class);
$smsdata = [];
$factorinstance = $smsfactor->setup_user_factor((object) $smsdata);
// Check if user factor was created successful.
$this->assertNotEmpty($factorinstance);
$this->assertEquals(1, count($smsfactor->get_active_user_factors($user)));
// Create the secret code.
$secretmanager = new \tool_mfa\local\secret_manager('sms');
$secretcode = $secretmanager->create_secret(1800, true);
// Check verification code.
$rcm = $rc->getMethod('check_verification_code');
$this->assertTrue($rcm->invoke($smsfactor, $secretcode));
// Test that calling the revoke on the generic type revokes all.
$smsfactor->revoke_user_factor($factorinstance->id);
$this->assertEquals(0, count($smsfactor->get_active_user_factors($user)));
unset($SESSION->tool_mfa_sms_number);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_sms
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_sms'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,95 @@
<?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 factor_token\event;
use stdClass;
/**
* Event for a token being created for a user.
*
* @package factor_token
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class token_created extends \core\event\base {
/**
* Create instance of event.
*
* @param stdClass $user the User object of the User who had the token creeated.
* @param array $state an array of the state of the token.
*
* @return \core\event\base the token_created_event event
*
* @throws \coding_exception
*/
public static function token_created_event(stdClass $user, array $state): \core\event\base {
$data = [
'relateduserid' => $user->id,
'context' => \context_user::instance($user->id),
'other' => [
'userid' => $user->id,
'state' => json_encode($state),
],
];
return self::create($data);
}
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'c';
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
$info = json_decode($this->other['state']);
$string = '<br>';
foreach ($info as $name => $value) {
if ($name === 'expiry') {
$value = userdate($value);
}
$string .= ucwords($name) . ': ' . $value . '<br>';
}
$data = new stdClass();
$data->string = $string;
$data->userid = $this->other['userid'];
return get_string('tokenstoredindevice', 'factor_token', $data);
}
/**
* Return localised event name.
*
* @return string
* @throws \coding_exception
*/
public static function get_name(): string {
return get_string('event:token_created', 'factor_token');
}
}
@@ -0,0 +1,256 @@
<?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 factor_token;
use stdClass;
use tool_mfa\local\factor\object_factor_base;
use tool_mfa\local\secret_manager;
/**
* Token factor class.
*
* @package factor_token
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor extends object_factor_base {
/**
* Token implementation.
*
* {@inheritDoc}
*/
public function has_input(): bool {
return false;
}
/**
* Token implementation.
* This factor is a singleton, return single instance.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors(stdClass $user): array {
global $DB;
$records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
if (!empty($records)) {
return $records;
}
// Null records returned, build new record.
$record = [
'userid' => $user->id,
'factor' => $this->name,
'timecreated' => time(),
'createdfromip' => $user->lastip,
'timemodified' => time(),
'revoked' => 0,
];
$record['id'] = $DB->insert_record('tool_mfa', $record, true);
return [(object) $record];
}
/**
* Token implementation.
* Checks whether the user has selected roles in any context.
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
// Check if there was a previous locked status to return.
$state = parent::get_state();
if ($state === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
return \tool_mfa\plugininfo\factor::STATE_LOCKED;
}
// Check cookie Exists.
$cookie = 'MFA_TOKEN_' . $USER->id;
if (NO_MOODLE_COOKIES || empty($_COOKIE[$cookie])) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
$token = $_COOKIE[$cookie];
$secretmanager = new secret_manager($this->name);
$verified = $secretmanager->validate_secret($token, true);
// If we got a bad cookie value, someone is likely being dodgy.
// In this instance we should just lock and make the user re-MFA.
if ($verified === secret_manager::NONVALID) {
$this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
return \tool_mfa\plugininfo\factor::STATE_LOCKED;
} else if ($verified === secret_manager::VALID) {
return \tool_mfa\plugininfo\factor::STATE_PASS;
}
// We should never get here. Factor cannot be revoked.
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
/**
* Token Implementation.
* We can't get_state like the parent here or it will recurse forever.
*
* @param string $state the state constant to set
* @return bool
*/
public function set_state($state): bool {
global $SESSION;
$property = 'factor_' . $this->name;
$SESSION->$property = $state;
return true;
}
/**
* Token implementation.
*
* @param stdClass $user
* @return array
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
\tool_mfa\plugininfo\factor::STATE_LOCKED,
];
}
/**
* Token implementation.
* Inject a checkbox into every auth form if needed.
*
* @param \MoodleQuickForm $mform Form to inject global elements into.
* @return void
*/
public function global_definition_after_data($mform): void {
global $SESSION;
// First thing, we need to decide on whether we should show the checkbox.
$noproperty = !property_exists($SESSION, 'tool_mfa_factor_token');
$nostate = $this->get_state() !== \tool_mfa\plugininfo\factor::STATE_PASS;
if ($noproperty && $nostate) {
$expiry = get_config('factor_token', 'expiry');
$expirystring = format_time($expiry);
$mform->addElement('advcheckbox', 'factor_token_trust', '', get_string('form:trust', 'factor_token', $expirystring));
$mform->setType('factor_token_trust', PARAM_BOOL);
$mform->setDefault('factor_token_trust', true);
}
}
/**
* Token implementation.
* Store information about the token status.
*
* @param object $data Data from the form.
* @return void
*/
public function global_submit($data): void {
global $SESSION;
// Store any kind of response here, we shouldnt show again.
$trust = $data->factor_token_trust;
$SESSION->tool_mfa_factor_token = $trust;
}
/**
* Token implementation.
* Pass hook to set the cookie for use in subsequent auths.
*
* {@inheritDoc}
*/
public function post_pass_state(): void {
global $CFG, $SESSION, $USER;
if (!property_exists($SESSION, 'tool_mfa_factor_token')) {
return;
}
$settoken = $SESSION->tool_mfa_factor_token;
if (!$settoken) {
return;
}
$cookie = 'MFA_TOKEN_' . $USER->id;
list($expirytime, $expiry) = $this->calculate_expiry_time();
// Store this secret in the database.
$secretmanager = new secret_manager($this->name);
$secret = base64_encode(random_bytes(256));
$secretmanager->create_secret($expiry, false, $secret);
// All the prep is now done, we can set this cookie.
setcookie($cookie, $secret, $expirytime, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, false, true);
// Finally emit a log event for storing the cookie.
$state = [
'expiry' => $expirytime,
'cookie' => $cookie,
];
$event = \factor_token\event\token_created::token_created_event($USER, $state);
$event->trigger();
}
/**
* Calculate the expiry time of the token, based on configuration.
*
* @param integer|null $basetime time to use for calcalations.
* @return array
*/
public function calculate_expiry_time($basetime = null): array {
if (empty($basetime)) {
$basetime = time();
}
// Calculate the expiry time. This is provided by config,
// But optionally might need to be rounded to expire a few hours after 0000 server time.
$expiry = get_config('factor_token', 'expiry');
$expirytime = $basetime + $expiry;
// If expiring overnight, it should expire at 2am the following morning, if required.
$expireovernight = get_config('factor_token', 'expireovernight');
if ($expireovernight) {
// Find out what 2am the following morning time is.
$datetime = new \DateTime();
$timezone = \core_date::get_user_timezone_object();
// Bit to ensure 'expireovernight' works when 'expire' is longer than one day.
$difftime = 0;
if ($expiry > DAYSECS) {
// Ensures a safe amount of days is added before doing the 2am checks.
$difftime = $expiry - DAYSECS;
}
// Calculte the overnight expiry time, ignoring 'expiry' duration period.
$workingexpirytime = $basetime + $difftime;
$datetime->setTimezone($timezone);
$datetime->setTimestamp($workingexpirytime);
$datetime->add(new \DateInterval('P1D'));
$datetime->setTime(2, 0); // Set the hour to 2am.
// Ensure whatever happens, ensure the expiry never goes over the default 'expiry' time.
$overnightexpirytime = $datetime->getTimestamp();
$expirytime = min($overnightexpirytime, $expirytime);
$expiry = $expirytime - $basetime;
}
return [$expirytime, $expiry];
}
}
@@ -0,0 +1,40 @@
<?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 factor_token\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_token
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,35 @@
<?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/>.
/**
* Language strings.
*
* @package factor_token
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['event:token_created'] = 'MFA token created.';
$string['form:trust'] = 'Trust this device for {$a}.';
$string['pluginname'] = 'Trust this device';
$string['privacy:metadata'] = 'The Trust this device factor plugin does not store any personal data.';
$string['settings:expireovernight'] = 'Expire trust overnight';
$string['settings:expireovernight_help'] = 'This forces tokens to expire overnight, preventing midday interruptions for users. Instead they will be asked to multi-factor authenticate at the start of a day after expiry.';
$string['settings:expiry'] = 'Trust duration';
$string['settings:expiry_help'] = 'The duration a device is trusted before requiring a new multi-factor authentication.';
$string['summarycondition'] = 'the user has previously trusted this device';
$string['tokenstoredindevice'] = 'The user with ID {$a->userid} has a multi-factor authentication token stored on their device. <br> Information: {$a->string}.';
+47
View File
@@ -0,0 +1,47 @@
<?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/>.
/**
* Settings
*
* @package factor_token
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$enabled = new admin_setting_configcheckbox('factor_token/enabled',
new lang_string('settings:enablefactor', 'tool_mfa'),
new lang_string('settings:enablefactor_help', 'tool_mfa'), 0);
$enabled->set_updatedcallback(function () {
\tool_mfa\manager::do_factor_action('token', get_config('factor_token', 'enabled') ? 'enable' : 'disable');
});
$settings->add($enabled);
$settings->add(new admin_setting_configtext('factor_token/weight',
new lang_string('settings:weight', 'tool_mfa'),
new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT));
$settings->add(new admin_setting_configduration('factor_token/expiry',
new lang_string('settings:expiry', 'factor_token'),
new lang_string('settings:expiry_help', 'factor_token'), DAYSECS));
$settings->add(new admin_setting_configcheckbox('factor_token/expireovernight',
new lang_string('settings:expireovernight', 'factor_token'),
new lang_string('settings:expireovernight_help', 'factor_token'), 1));
@@ -0,0 +1,286 @@
<?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 factor_token;
/**
* Tests for MFA manager class.
*
* @package factor_token
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @author Kevin Pham <kevinpham@catalyst-au.net>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factor_test extends \advanced_testcase {
/**
* Holds specific requested factor, which is token factor.
*
* @var \factor_token\factor $factor
*/
public \factor_token\factor $factor;
public function setUp(): void {
$this->resetAfterTest();
$this->factor = new \factor_token\factor('token');
}
/**
* Test calculating expiry time in general
*
* @covers ::calculate_expiry_time
* @return void
*/
public function test_calculate_expiry_time_in_general(): void {
$timestamp = 1642213800; // 1230 UTC.
set_config('expireovernight', 0, 'factor_token');
$method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
// Test that non-overnight timestamps are just exactly as configured.
// We don't need to care about 0 or negative ints, they will just make the cookie expire immediately.
$expiry = $method->invoke($this->factor, $timestamp);
$this->assertEquals(DAYSECS, $expiry[1]);
set_config('expiry', HOURSECS, 'factor_token');
$expiry = $method->invoke($this->factor, $timestamp);
$this->assertGreaterThan(HOURSECS - 30, $expiry[1]);
$this->assertLessThan(HOURSECS + 30, $expiry[1]);
set_config('expireovernight', 1, 'factor_token');
// Manually calculate the next reset time.
$reset = strtotime('tomorrow 0200', $timestamp);
$resetdelta = $reset - $timestamp;
// Confirm that a timestamp that doesnt reach reset time.
if ($timestamp + HOURSECS < $reset) {
$expiry = $method->invoke($this->factor, $timestamp);
$this->assertGreaterThan(HOURSECS - 30, $expiry[1]);
$this->assertLessThan(HOURSECS + 30, $expiry[1]);
}
set_config('expiry', 2 * DAYSECS, 'factor_token');
// Now confirm that the returned expiry is less than the absolute amount.
$expiry = $method->invoke($this->factor, $timestamp);
$this->assertGreaterThan(DAYSECS, $expiry[1]);
$this->assertLessThan(2 * DAYSECS, $expiry[1]);
$this->assertGreaterThan($resetdelta + DAYSECS - 30, $expiry[1]);
$this->assertLessThan($resetdelta + DAYSECS + 30, $expiry[1]);
}
/**
* Everything should end at 2am unless adding the hours lands it between
* 0 <= x < 2am, which in that case it should just expire using the raw
* value, provided it never goes past raw value expiry time, and when it
* needs to be 2am, it's 2am on the following morning.
*
* @covers ::calculate_expiry_time
* @param int $timestamp
* @dataProvider timestamp_provider
*/
public function test_calculate_expiry_time_for_overnight_expiry_with_one_day_expiry($timestamp): void {
// Setup configuration.
$method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
set_config('expireovernight', 1, 'factor_token');
set_config('expiry', DAYSECS, 'factor_token');
// All the results here, should be for 2am the following morning from the timestamp provided.
$expiry = $method->invoke($this->factor, $timestamp);
list($expiresat, $secondstillexpiry) = $expiry;
// Calculate the expected raw expiry if not considering 'overnight'.
$timezone = \core_date::get_user_timezone_object();
$datetime = new \DateTime();
$datetime->setTimezone($timezone);
$rawexpiry = $timestamp + DAYSECS;
$datetime->setTimestamp($rawexpiry);
$rawhour = $datetime->format('H');
$rawminute = $datetime->format('m');
// Sanity check, that the $secondstillexpiry is in the appropriate ranges.
$this->assertGreaterThan(0, $secondstillexpiry);
$this->assertLessThan(DAYSECS + 1, $secondstillexpiry);
if ($rawhour >= 0 && $rawhour < 2 || $rawhour == 2 && $rawminute == 0) {
// Should just use expiry time, if the hours will land between 0 and 2am.
$this->assertEquals($datetime->getTimestamp(), $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
} else {
// Otherwise it should fall on 2am the following day.
$followingdayattwoam = strtotime('tomorrow 0200', $timestamp);
$this->assertEquals($followingdayattwoam, $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
}
}
/**
* Everything should end at 2am unless adding the hours lands it between
* 0 <= x < 2am, which in that case it should just expire using the raw
* value, provided it never goes past raw value expiry time, and when it
* needs to be 2am, it's 2am on the morning after tomorrow.
*
* @covers ::calculate_expiry_time
* @param int $timestamp
* @dataProvider timestamp_provider
*/
public function test_calculate_expiry_time_for_overnight_expiry_with_two_day_expiry($timestamp): void {
// Setup configuration.
$method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
set_config('expireovernight', 1, 'factor_token');
set_config('expiry', 2 * DAYSECS, 'factor_token');
// All the results here, should be for 2am the following morning from the timestamp provided.
$expiry = $method->invoke($this->factor, $timestamp);
list($expiresat, $secondstillexpiry) = $expiry;
// Calculate the expected raw expiry if not considering 'overnight'.
$timezone = \core_date::get_user_timezone_object();
$datetime = new \DateTime();
$datetime->setTimezone($timezone);
$rawexpiry = $timestamp + (2 * DAYSECS);
$datetime->setTimestamp($rawexpiry);
$rawhour = $datetime->format('H');
$rawminute = $datetime->format('m');
// Sanity check, that the $secondstillexpiry is in the appropriate ranges.
$this->assertGreaterThan(0, $secondstillexpiry);
$this->assertLessThan((2 * DAYSECS) + 1, $secondstillexpiry);
if ($rawhour >= 0 && $rawhour < 2 || $rawhour == 2 && $rawminute == 0) {
// Should just use expiry time, if the hours will land between 0 and 2am.
$this->assertEquals($datetime->getTimestamp(), $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
} else {
// Otherwise it should fall on 2am the following day after tomorrow.
$followingdayattwoam = strtotime('tomorrow 0200', $timestamp) + DAYSECS;
$this->assertEquals($followingdayattwoam, $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
}
// Expiry should always be more than one day for an expiry duration of
// more than 1 day, but the overnight check should apply for the
// duration of the final night.
$this->assertGreaterThan(DAYSECS, $secondstillexpiry);
}
/**
* This should check if the 3am expiry is pushed back to 2am as expected, but everything else appears as expected
*
* @covers ::calculate_expiry_time
* @param int $timestamp
* @dataProvider timestamp_provider
*/
public function test_calculate_expiry_time_for_overnight_expiry_with_three_hour_expiry($timestamp): void {
// Setup configuration.
$method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
set_config('expireovernight', 1, 'factor_token');
set_config('expiry', 3 * HOURSECS, 'factor_token');
// All the results here, should be for 2am the following morning from the timestamp provided.
$expiry = $method->invoke($this->factor, $timestamp);
list($expiresat, $secondstillexpiry) = $expiry;
// Calculate the expected raw expiry if not considering 'overnight'.
$timezone = \core_date::get_user_timezone_object();
$datetime = new \DateTime();
$datetime->setTimezone($timezone);
$rawexpiry = $timestamp + (3 * HOURSECS);
$datetime->setTimestamp($rawexpiry);
// Sanity check, that the $secondstillexpiry is in the appropriate ranges.
$this->assertGreaterThan(0, $secondstillexpiry);
$this->assertLessThan((3 * HOURSECS) + 1, $secondstillexpiry);
// If the raw timestamp of the expiry, is less than tomorrow at 2am,
// then use the raw expiry time.
$followingdayattwoam = strtotime('tomorrow 0200', $timestamp);
if ($datetime->getTimestamp() < $followingdayattwoam) {
$this->assertEquals($datetime->getTimestamp(), $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
} else {
// Otherwsie it should be pushed back to 2am.
$this->assertEquals($followingdayattwoam, $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
}
}
/**
* Only relevant based on the hour padding used, which is currently set to 2 hours (2am).
*
* @covers ::calculate_expiry_time
* @param int $timestamp
* @dataProvider timestamp_provider
*/
public function test_calculate_expiry_time_for_overnight_expiry_with_an_hour_expiry($timestamp): void {
// Setup configuration.
$method = new \ReflectionMethod($this->factor, 'calculate_expiry_time');
set_config('expireovernight', 1, 'factor_token');
set_config('expiry', HOURSECS, 'factor_token');
// All the results here, should be for 2am the following morning from the timestamp provided.
$expiry = $method->invoke($this->factor, $timestamp);
list($expiresat, $secondstillexpiry) = $expiry;
// Calculate the expected raw expiry if not considering 'overnight'.
$timezone = \core_date::get_user_timezone_object();
$datetime = new \DateTime();
$datetime->setTimezone($timezone);
$rawexpiry = $timestamp + HOURSECS;
$datetime->setTimestamp($rawexpiry);
// Sanity check, that the $secondstillexpiry is in the appropriate ranges.
$this->assertGreaterThan(0, $secondstillexpiry);
$this->assertLessThan(HOURSECS + 1, $secondstillexpiry);
// If the raw timestamp of the expiry, is less than tomorrow at 2am,
// then use the raw expiry time.
$followingdayattwoam = strtotime('tomorrow 0200', $timestamp);
if ($datetime->getTimestamp() < $followingdayattwoam) {
$this->assertEquals($datetime->getTimestamp(), $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($expiresat - $timestamp, $secondstillexpiry);
} else {
// Otherwsie it should be pushed back to 2am.
$this->assertEquals($followingdayattwoam, $expiresat);
// Ensure the $secondstillexpiry is calculated correctly.
$this->assertEquals($followingdayattwoam - $timestamp, $secondstillexpiry);
}
}
/**
* Timestamps for a 24 hour period starting from a fixed time.
* Increments by 30 minutes to cover half hour and hour cases.
* Starting timestamp: 2022-01-15 07:30:00 Australia/Melbourne time.
*/
public function timestamp_provider() {
$starttimestamp = 1642192200;
foreach (range(0, 23) as $i) {
$timestamps[] = [$starttimestamp + ($i * HOURSECS)];
$timestamps[] = [$starttimestamp + ($i * HOURSECS) + (30 * MINSECS)];
}
return $timestamps;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Plugin version and other meta-data are defined here.
*
* @package factor_token
* @subpackage 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'factor_token'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
@@ -0,0 +1,503 @@
<?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 factor_totp;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tcpdf/tcpdf_barcodes_2d.php');
require_once(__DIR__.'/../extlib/OTPHP/OTPInterface.php');
require_once(__DIR__.'/../extlib/OTPHP/TOTPInterface.php');
require_once(__DIR__.'/../extlib/OTPHP/ParameterTrait.php');
require_once(__DIR__.'/../extlib/OTPHP/OTP.php');
require_once(__DIR__.'/../extlib/OTPHP/TOTP.php');
require_once(__DIR__.'/../extlib/Assert/Assertion.php');
require_once(__DIR__.'/../extlib/Assert/AssertionFailedException.php');
require_once(__DIR__.'/../extlib/Assert/InvalidArgumentException.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/EncoderInterface.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Binary.php');
require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Base32.php');
use tool_mfa\local\factor\object_factor_base;
use OTPHP\TOTP;
use stdClass;
/**
* TOTP factor class.
*
* @package factor_totp
* @subpackage 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 extends object_factor_base {
/** @var string */
const TOTP_OLD = 'old';
/** @var string */
const TOTP_FUTURE = 'future';
/** @var string */
const TOTP_USED = 'used';
/** @var string */
const TOTP_VALID = 'valid';
/** @var string */
const TOTP_INVALID = 'invalid';
/** @var string Factor icon */
protected $icon = 'fa-mobile-screen';
/**
* Generates TOTP URI for given secret key.
* Uses site name, hostname and user name to make GA account look like:
* "Sitename hostname (username)".
*
* @param string $secret
* @return string
*/
public function generate_totp_uri(string $secret): string {
global $USER, $SITE, $CFG;
$host = parse_url($CFG->wwwroot, PHP_URL_HOST);
$sitename = str_replace(':', '', $SITE->fullname);
$issuer = $sitename.' '.$host;
$totp = TOTP::create($secret);
$totp->setLabel($USER->username);
$totp->setIssuer($issuer);
return $totp->getProvisioningUri();
}
/**
* Generates HTML sting with QR code for given secret key.
*
* @param string $secret
* @return string
*/
public function generate_qrcode(string $secret): string {
$uri = $this->generate_totp_uri($secret);
$qrcode = new \TCPDF2DBarcode($uri, 'QRCODE');
$image = $qrcode->getBarcodePngData(7, 7);
$html = \html_writer::img('data:image/png;base64,' . base64_encode($image), '', ['width' => '150px']);
return $html;
}
/**
* TOTP state
*
* {@inheritDoc}
*/
public function get_state(): string {
global $USER;
$userfactors = $this->get_active_user_factors($USER);
// If no codes are setup then we must be neutral not unknown.
if (count($userfactors) == 0) {
return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
}
return parent::get_state();
}
/**
* TOTP Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
$secret = $this->generate_secret_code();
$mform->addElement('hidden', 'secret', $secret);
$mform->setType('secret', PARAM_ALPHANUM);
return $mform;
}
/**
* TOTP Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
global $OUTPUT, $SITE, $USER;
// Array of elements to allow XSS.
$xssallowedelements = [];
$headingstring = $mform->elementExists('replaceid') ? 'replacefactor' : 'setupfactor';
$mform->addElement('html', $OUTPUT->heading(get_string($headingstring, 'factor_totp'), 2));
$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->setType('devicename', PARAM_TEXT);
$mform->addRule('devicename', get_string('required'), 'required', null, 'client');
$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);
$mform->addElement('static', 'scan', '', $html);
// Enter manually.
$secret = wordwrap($secret, 4, ' ', true) . '</code>';
$secret = \html_writer::tag('code', $secret);
$manualtable = new \html_table();
$manualtable->id = 'manualattributes';
$manualtable->attributes['class'] = 'generaltable table table-bordered table-sm w-auto';
$manualtable->attributes['style'] = 'width: auto;';
$manualtable->data = [
[get_string('setupfactor:key', 'factor_totp'), $secret],
[get_string('setupfactor:account', 'factor_totp'), "$SITE->fullname ($USER->username)"],
[get_string('setupfactor:mode', 'factor_totp'), get_string('setupfactor:mode:timebased', 'factor_totp')],
];
$html = \html_writer::table($manualtable);
// Wrap the table in a couple of divs to be controlled via bootstrap.
$html = \html_writer::div($html, 'collapse', ['id' => 'collapseManualAttributes']);
$togglelink = \html_writer::tag('a', get_string('setupfactor:link', 'factor_totp'), [
'data-toggle' => 'collapse',
'data-target' => '#collapseManualAttributes',
'aria-expanded' => 'false',
'aria-controls' => 'collapseManualAttributes',
'href' => '#',
]);
$html = $togglelink . $html;
$xssallowedelements[] = $mform->addElement('static', 'enter', '', $html);
// Allow XSS.
if (method_exists('MoodleQuickForm_static', 'set_allow_xss')) {
foreach ($xssallowedelements as $xssallowedelement) {
$xssallowedelement->set_allow_xss(true);
}
}
// 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->addRule('verificationcode', get_string('required'), 'required', null, 'client');
return $mform;
}
/**
* TOTP Factor implementation.
*
* @param array $data
* @return array
*/
public function setup_factor_form_validation(array $data): array {
$errors = [];
$totp = TOTP::create($data['secret']);
if (!$totp->verify($data['verificationcode'], time(), 1)) {
$errors['verificationcode'] = get_string('error:wrongverification', 'factor_totp');
}
return $errors;
}
/**
* TOTP Factor implementation.
*
* @param \MoodleQuickForm $mform
* @return \MoodleQuickForm $mform
*/
public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
$mform->disable_form_change_checker();
$mform->addElement(new \tool_mfa\local\form\verification_field());
$mform->setType('verificationcode', PARAM_ALPHANUM);
return $mform;
}
/**
* TOTP Factor implementation.
*
* @param array $data
* @return array
*/
public function login_form_validation(array $data): array {
global $USER;
$factors = $this->get_active_user_factors($USER);
$result = ['verificationcode' => get_string('error:wrongverification', 'factor_totp')];
$windowconfig = get_config('factor_totp', 'window');
foreach ($factors as $factor) {
$totp = TOTP::create($factor->secret);
// Convert seconds to windows.
$window = (int) floor($windowconfig / $totp->getPeriod());
$factorresult = $this->validate_code($data['verificationcode'], $window, $totp, $factor);
$time = userdate(time(), get_string('systimeformat', 'factor_totp'));
switch ($factorresult) {
case self::TOTP_USED:
return ['verificationcode' => get_string('error:codealreadyused', 'factor_totp')];
case self::TOTP_OLD:
return ['verificationcode' => get_string('error:oldcode', 'factor_totp', $time)];
case self::TOTP_FUTURE:
return ['verificationcode' => get_string('error:futurecode', 'factor_totp', $time)];
case self::TOTP_VALID:
$this->update_lastverified($factor->id);
return [];
default:
continue(2);
}
}
return $result;
}
/**
* Checks the code for reuse, clock skew, and validity.
*
* @param string $code the code to check.
* @param int $window the window to check validity for.
* @param TOTP $totp the totp object to check against.
* @param stdClass $factor the factor with information required.
*
* @return string constant with verification state.
*/
public function validate_code(string $code, int $window, TOTP $totp, stdClass $factor): string {
// First check if this code matches the last verified timestamp.
$lastverified = $this->get_lastverified($factor->id);
if ($lastverified > 0 && $totp->verify($code, $lastverified, $window)) {
return self::TOTP_USED;
}
// The window in which to check for clock skew, 5 increments past valid window.
$skewwindow = $window + 5;
$pasttimestamp = time() - ($skewwindow * $totp->getPeriod());
$futuretimestamp = time() + ($skewwindow * $totp->getPeriod());
if ($totp->verify($code, time(), $window)) {
return self::TOTP_VALID;
} else if ($totp->verify($code, $pasttimestamp, $skewwindow)) {
// Check for clock skew in the past 10 periods.
return self::TOTP_OLD;
} else if ($totp->verify($code, $futuretimestamp, $skewwindow)) {
// Check for clock skew in the future 10 periods.
return self::TOTP_FUTURE;
} else {
// In all other cases, code is invalid.
return self::TOTP_INVALID;
}
}
/**
* Generates cryptographically secure pseudo-random 16-digit secret code.
*
* @return string
*/
public function generate_secret_code(): string {
$totp = TOTP::create();
return substr($totp->getSecret(), 0, 16);
}
/**
* TOTP Factor implementation.
*
* @param stdClass $data
* @return stdClass|null the factor record, or null.
*/
public function setup_user_factor(stdClass $data): stdClass|null {
global $DB, $USER;
if (!empty($data->secret)) {
$row = new stdClass();
$row->userid = $USER->id;
$row->factor = $this->name;
$row->secret = $data->secret;
$row->label = $data->devicename;
$row->timecreated = time();
$row->createdfromip = $USER->lastip;
$row->timemodified = time();
$row->lastverified = 0;
$row->revoked = 0;
// Check if a record with this configuration already exists, warning the user accordingly.
$record = $DB->get_record('tool_mfa', [
'userid' => $row->userid,
'secret' => $row->secret,
'factor' => $row->factor,
], '*', IGNORE_MULTIPLE);
if ($record) {
\core\notification::warning(get_string('error:alreadyregistered', 'factor_totp'));
return null;
}
$id = $DB->insert_record('tool_mfa', $row);
$record = $DB->get_record('tool_mfa', ['id' => $id]);
$this->create_event_after_factor_setup($USER);
return $record;
}
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.
*
* @param stdClass $user the user to check against.
* @return array
*/
public function get_all_user_factors($user): array {
global $DB;
return $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
}
/**
* TOTP Factor implementation.
*
* {@inheritDoc}
*/
public function has_revoke(): bool {
return true;
}
/**
* TOTP Factor implementation.
*/
public function has_replace(): bool {
return true;
}
/**
* TOTP Factor implementation.
*
* {@inheritDoc}
*/
public function has_setup(): bool {
return true;
}
/**
* TOTP Factor implementation
*
* {@inheritDoc}
*/
public function show_setup_buttons(): bool {
return true;
}
/**
* TOTP Factor implementation.
* Empty override of parent.
*
* {@inheritDoc}
*/
public function post_pass_state(): void {
return;
}
/**
* TOTP Factor implementation.
* TOTP cannot return fail state.
*
* @param stdClass $user
*/
public function possible_states(stdClass $user): array {
return [
\tool_mfa\plugininfo\factor::STATE_PASS,
\tool_mfa\plugininfo\factor::STATE_NEUTRAL,
\tool_mfa\plugininfo\factor::STATE_UNKNOWN,
];
}
/**
* TOTP Factor implementation.
*
* {@inheritDoc}
*/
public function get_setup_string(): string {
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');
}
}
@@ -0,0 +1,40 @@
<?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 factor_totp\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy provider.
*
* @package factor_totp
* @author Mikhail Golenkov <golenkovm@gmail.com>
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
<?php
/**
* Assert
*
* LICENSE
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to kontakt@beberlei.de so I can send you a copy immediately.
*/
namespace Assert;
use Throwable;
interface AssertionFailedException extends Throwable
{
/**
* @return string|null
*/
public function getPropertyPath();
/**
* @return mixed
*/
public function getValue();
/**
* @return array
*/
public function getConstraints(): array;
}
@@ -0,0 +1,76 @@
<?php
/**
* Assert
*
* LICENSE
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to kontakt@beberlei.de so I can send you a copy immediately.
*/
namespace Assert;
class InvalidArgumentException extends \InvalidArgumentException implements AssertionFailedException
{
/**
* @var string|null
*/
private $propertyPath;
/**
* @var mixed
*/
private $value;
/**
* @var array
*/
private $constraints;
public function __construct($message, $code, string $propertyPath = null, $value = null, array $constraints = [])
{
parent::__construct($message, $code);
$this->propertyPath = $propertyPath;
$this->value = $value;
$this->constraints = $constraints;
}
/**
* User controlled way to define a sub-property causing
* the failure of a currently asserted objects.
*
* Useful to transport information about the nature of the error
* back to higher layers.
*
* @return string|null
*/
public function getPropertyPath()
{
return $this->propertyPath;
}
/**
* Get the value that caused the assertion to fail.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Get the constraints that applied to the failed assertion.
*
* @return array
*/
public function getConstraints(): array
{
return $this->constraints;
}
}
@@ -0,0 +1,11 @@
Copyright (c) 2011-2013, Benjamin Eberlei
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
@@ -0,0 +1,23 @@
{
"name": "beberlei/assert",
"description": "Thin assertion library for input validation in business models.",
"authors": [
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}
],
"license": "BSD-2-Clause",
"keywords": ["assert", "assertion", "validation"],
"require": {
"ext-mbstring": "*"
},
"autoload": {
"psr-0": {
"Assert": "lib/"
},
"files": ["lib/Assert/functions.php"]
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
}
}
@@ -0,0 +1,17 @@
Assert 2.1
--------------
https://github.com/beberlei/assert/releases/tag/v2.1
Instructions to import WebAuthn into Moodle:
1. Download the latest release from https://github.com/beberlei/assert/releases/tag/vx.x
(choose "Source code")
2. Unzip the source code
3. Copy the following files from assert-x.x/lib/Assert into admin/tool/mfa/factor/totp/extlib/Assert:
1. Assertion.php
2. AssertionFailedException.php
3. InvalidArgumentException.php
4. Copy the following files from assert-x.x into admin/tool/mfa/factor/totp/extlib/Assert:
1. LICENSE
2. composer.json
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Florent Morselli
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
use Assert\Assertion;
use ParagonIE\ConstantTime\Base32;
abstract class OTP implements OTPInterface
{
use ParameterTrait;
/**
* OTP constructor.
*
* @param string|null $secret
* @param string $digest
* @param int $digits
*/
protected function __construct($secret, string $digest, int $digits)
{
$this->setSecret($secret);
$this->setDigest($digest);
$this->setDigits($digits);
}
/**
* {@inheritdoc}
*/
public function getQrCodeUri(string $uri = 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl={PROVISIONING_URI}', string $placeholder = '{PROVISIONING_URI}'): string
{
$provisioning_uri = urlencode($this->getProvisioningUri());
return str_replace($placeholder, $provisioning_uri, $uri);
}
/**
* @param int $input
*
* @return string The OTP at the specified input
*/
protected function generateOTP(int $input): string
{
$hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret());
$hmac = [];
foreach (str_split($hash, 2) as $hex) {
$hmac[] = hexdec($hex);
}
$offset = $hmac[count($hmac) - 1] & 0xF;
$code = ($hmac[$offset + 0] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
$otp = $code % pow(10, $this->getDigits());
return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);
}
/**
* {@inheritdoc}
*/
public function at(int $timestamp): string
{
return $this->generateOTP($timestamp);
}
/**
* @param array $options
*/
protected function filterOptions(array &$options)
{
foreach (['algorithm' => 'sha1', 'period' => 30, 'digits' => 6] as $key => $default) {
if (isset($options[$key]) && $default === $options[$key]) {
unset($options[$key]);
}
}
ksort($options);
}
/**
* @param string $type
* @param array $options
*
* @return string
*/
protected function generateURI(string $type, array $options): string
{
$label = $this->getLabel();
Assertion::string($label, 'The label is not set.');
Assertion::false($this->hasColon($label), 'Label must not contain a colon.');
$options = array_merge($options, $this->getParameters());
$this->filterOptions($options);
$params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
return sprintf('otpauth://%s/%s?%s', $type, rawurlencode((null !== $this->getIssuer() ? $this->getIssuer().':' : '').$label), $params);
}
/**
* @return string
*/
private function getDecodedSecret(): string
{
try {
$secret = Base32::decodeUpper($this->getSecret());
} catch (\Exception $e) {
throw new \RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
}
return $secret;
}
/**
* @param int $int
*
* @return string
*/
private function intToByteString(int $int): string
{
$result = [];
while (0 !== $int) {
$result[] = chr($int & 0xFF);
$int >>= 8;
}
return str_pad(implode(array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
/**
* @param string $safe
* @param string $user
*
* @return bool
*/
protected function compareOTP(string $safe, string $user): bool
{
return hash_equals($safe, $user);
}
}
@@ -0,0 +1,123 @@
<?php
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
interface OTPInterface
{
/**
* @param int $timestamp
*
* @return string Return the OTP at the specified timestamp
*/
public function at(int $timestamp): string;
/**
* Verify that the OTP is valid with the specified input.
* If no input is provided, the input is set to a default value or false is returned.
*
* @param string $otp
* @param int|null $input
* @param int|null $window
*
* @return bool
*/
public function verify(string $otp, $input = null, $window = null): bool;
/**
* @return string The secret of the OTP
*/
public function getSecret(): string;
/**
* @param string $label The label of the OTP
*/
public function setLabel(string $label);
/**
* @return string|null The label of the OTP
*/
public function getLabel();
/**
* @return string|null The issuer
*/
public function getIssuer();
/**
* @param string $issuer
*
* @throws \InvalidArgumentException
*/
public function setIssuer(string $issuer);
/**
* @return bool If true, the issuer will be added as a parameter in the provisioning URI
*/
public function isIssuerIncludedAsParameter(): bool;
/**
* @param bool $issuer_included_as_parameter
*
* @return $this
*/
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter);
/**
* @return int Number of digits in the OTP
*/
public function getDigits(): int;
/**
* @return string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'
*/
public function getDigest(): string;
/**
* @param string $parameter
*
* @return null|mixed
*/
public function getParameter(string $parameter);
/**
* @param string $parameter
*
* @return bool
*/
public function hasParameter(string $parameter): bool;
/**
* @return array
*/
public function getParameters(): array;
/**
* @param string $parameter
* @param mixed $value
*
* @return $this
*/
public function setParameter(string $parameter, $value);
/**
* @return string Get the provisioning URI
*/
public function getProvisioningUri(): string;
/**
* @param string $uri The Uri of the QRCode generator with all parameters. By default the Googgle Chart API is used. This Uri MUST contain a placeholder that will be replaced by the method.
* @param string $placeholder The placeholder to be replaced in the QR Code generator URI. Default value is {PROVISIONING_URI}.
*
* @return string Get the provisioning URI
*/
public function getQrCodeUri(string $uri = 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl={PROVISIONING_URI}', string $placeholder = '{PROVISIONING_URI}'): string;
}
@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
use Assert\Assertion;
use ParagonIE\ConstantTime\Base32;
trait ParameterTrait
{
/**
* @var array
*/
private $parameters = [];
/**
* @var string|null
*/
private $issuer = null;
/**
* @var string|null
*/
private $label = null;
/**
* @var bool
*/
private $issuer_included_as_parameter = true;
/**
* @return array
*/
public function getParameters(): array
{
$parameters = $this->parameters;
if (null !== $this->getIssuer() && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
return $parameters;
}
/**
* @return string
*/
public function getSecret(): string
{
return $this->getParameter('secret');
}
/**
* @param string|null $secret
*/
private function setSecret($secret)
{
$this->setParameter('secret', $secret);
}
/**
* @return string|null
*/
public function getLabel()
{
return $this->label;
}
/**
* @param string $label
*/
public function setLabel(string $label)
{
$this->setParameter('label', $label);
}
/**
* @return string|null
*/
public function getIssuer()
{
return $this->issuer;
}
/**
* @param string $issuer
*/
public function setIssuer(string $issuer)
{
$this->setParameter('issuer', $issuer);
}
/**
* @return bool
*/
public function isIssuerIncludedAsParameter(): bool
{
return $this->issuer_included_as_parameter;
}
/**
* @param bool $issuer_included_as_parameter
*/
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter)
{
$this->issuer_included_as_parameter = $issuer_included_as_parameter;
}
/**
* @return int
*/
public function getDigits(): int
{
return $this->getParameter('digits');
}
/**
* @param int $digits
*/
private function setDigits(int $digits)
{
$this->setParameter('digits', $digits);
}
/**
* @return string
*/
public function getDigest(): string
{
return $this->getParameter('algorithm');
}
/**
* @param string $digest
*/
private function setDigest(string $digest)
{
$this->setParameter('algorithm', $digest);
}
/**
* @param string $parameter
*
* @return bool
*/
public function hasParameter(string $parameter): bool
{
return array_key_exists($parameter, $this->parameters);
}
/**
* @param string $parameter
*
* @return mixed
*/
public function getParameter(string $parameter)
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
}
throw new \InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
}
/**
* @param string $parameter
* @param mixed $value
*/
public function setParameter(string $parameter, $value)
{
$map = $this->getParameterMap();
if (true === array_key_exists($parameter, $map)) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (property_exists($this, $parameter)) {
$this->$parameter = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
/**
* @return array
*/
protected function getParameterMap(): array
{
return [
'label' => function ($value) {
Assertion::false($this->hasColon($value), 'Label must not contain a colon.');
return $value;
},
'secret' => function ($value) {
if (null === $value) {
$value = Base32::encodeUpper(random_bytes(64));
}
$value = trim(strtoupper($value), '=');
return $value;
},
'algorithm' => function ($value) {
$value = strtolower($value);
Assertion::inArray($value, hash_algos(), sprintf('The "%s" digest is not supported.', $value));
return $value;
},
'digits' => function ($value) {
Assertion::greaterThan($value, 0, 'Digits must be at least 1.');
return (int) $value;
},
'issuer' => function ($value) {
Assertion::false($this->hasColon($value), 'Issuer must not contain a colon.');
return $value;
},
];
}
/**
* @param string $value
*
* @return bool
*/
private function hasColon($value)
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (false !== strpos($value, $colon)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
use Assert\Assertion;
final class TOTP extends OTP implements TOTPInterface
{
/**
* TOTP constructor.
*
* @param string|null $secret
* @param int $period
* @param string $digest
* @param int $digits
* @param int $epoch
*/
protected function __construct($secret, int $period, string $digest, int $digits, int $epoch = 0)
{
parent::__construct($secret, $digest, $digits);
$this->setPeriod($period);
$this->setEpoch($epoch);
}
/**
* TOTP constructor.
*
* @param string|null $secret
* @param int $period
* @param string $digest
* @param int $digits
* @param int $epoch
*
* @return self
*/
public static function create($secret = null, int $period = 30, string $digest = 'sha1', int $digits = 6, int $epoch = 0): self
{
return new self($secret, $period, $digest, $digits, $epoch);
}
/**
* @param int $period
*/
protected function setPeriod(int $period)
{
$this->setParameter('period', $period);
}
/**
* {@inheritdoc}
*/
public function getPeriod(): int
{
return $this->getParameter('period');
}
/**
* @param int $epoch
*/
private function setEpoch(int $epoch)
{
$this->setParameter('epoch', $epoch);
}
/**
* {@inheritdoc}
*/
public function getEpoch(): int
{
return $this->getParameter('epoch');
}
/**
* {@inheritdoc}
*/
public function at(int $timestamp): string
{
return $this->generateOTP($this->timecode($timestamp));
}
/**
* {@inheritdoc}
*/
public function now(): string
{
return $this->at(time());
}
/**
* If no timestamp is provided, the OTP is verified at the actual timestamp
* {@inheritdoc}
*/
public function verify(string $otp, $timestamp = null, $window = null): bool
{
$timestamp = $this->getTimestamp($timestamp);
if (null === $window) {
return $this->compareOTP($this->at($timestamp), $otp);
}
return $this->verifyOtpWithWindow($otp, $timestamp, $window);
}
/**
* @param string $otp
* @param int $timestamp
* @param int $window
*
* @return bool
*/
private function verifyOtpWithWindow(string $otp, int $timestamp, int $window): bool
{
$window = abs($window);
for ($i = 0; $i <= $window; $i++) {
$next = (int) $i * $this->getPeriod() + $timestamp;
$previous = (int) -$i * $this->getPeriod() + $timestamp;
$valid = $this->compareOTP($this->at($next), $otp) ||
$this->compareOTP($this->at($previous), $otp);
if ($valid) {
return true;
}
}
return false;
}
/**
* @param int|null $timestamp
*
* @return int
*/
private function getTimestamp($timestamp): int
{
$timestamp = $timestamp ?? time();
Assertion::greaterOrEqualThan($timestamp, 0, 'Timestamp must be at least 0.');
return (int) $timestamp;
}
/**
* {@inheritdoc}
*/
public function getProvisioningUri(): string
{
$params = [];
if (30 !== $this->getPeriod()) {
$params['period'] = $this->getPeriod();
}
if (0 !== $this->getEpoch()) {
$params['epoch'] = $this->getEpoch();
}
return $this->generateURI('totp', $params);
}
/**
* @param int $timestamp
*
* @return int
*/
private function timecode(int $timestamp): int
{
return (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
}
/**
* {@inheritdoc}
*/
protected function getParameterMap(): array
{
$v = array_merge(
parent::getParameterMap(),
[
'period' => function ($value) {
Assertion::greaterThan((int) $value, 0, 'Period must be at least 1.');
return (int) $value;
},
'epoch' => function ($value) {
Assertion::greaterOrEqualThan((int) $value, 0, 'Epoch must be greater than or equal to 0.');
return (int) $value;
},
]
);
return $v;
}
/**
* {@inheritdoc}
*/
protected function filterOptions(array &$options)
{
parent::filterOptions($options);
if (isset($options['epoch']) && 0 === $options['epoch']) {
unset($options['epoch']);
}
ksort($options);
}
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2018 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace OTPHP;
interface TOTPInterface extends OTPInterface
{
/**
* @return string Return the TOTP at the current time
*/
public function now(): string;
/**
* @return int Get the period of time for OTP generation (a non-null positive integer, in second)
*/
public function getPeriod(): int;
}
@@ -0,0 +1,40 @@
{
"name": "spomky-labs/otphp",
"type": "library",
"description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
"license": "MIT",
"keywords": ["otp", "hotp", "totp", "RFC 4226", "RFC 6238", "Google Authenticator", "FreeOTP"],
"homepage": "https://github.com/Spomky-Labs/otphp",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/Spomky-Labs/otphp/contributors"
}
],
"require": {
"php": "^7.1",
"paragonie/constant_time_encoding": "^2.0",
"beberlei/assert": "^2.4"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"satooshi/php-coveralls": "^1.0"
},
"suggest": {
},
"autoload": {
"psr-4": { "OTPHP\\": "src/" }
},
"autoload-dev": {
"psr-4": { "OTPHP\\Test\\": "tests/" }
},
"extra": {
"branch-alias": {
"dev-master": "9.0.x-dev"
}
}
}
@@ -0,0 +1,19 @@
OTPHP 9.1.1
--------------
https://github.com/Spomky-Labs/otphp/releases/tag/v9.1.1
Instructions to import WebAuthn into Moodle:
1. Download the latest release from https://github.com/Spomky-Labs/otphp/releases/tag/vx.x.x
(choose "Source code")
2. Unzip the source code
3. Copy the following files from otphp-x.x/lib/OTPHP into admin/tool/mfa/factor/totp/extlib/OTPHP:
1. OTP.php
2. OTPInterface.php
3. ParameterTrait.php
4. TOTP.php
5. TOTPInterface.php
4. Copy the following files from otphp-x.x into admin/tool/mfa/factor/totp/extlib/OTPHP:
1. LICENSE
2. composer.json
@@ -0,0 +1,430 @@
<?php
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2017 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base32
* [A-Z][2-7]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base32 implements EncoderInterface
{
/**
* Decode a Base32-encoded string into raw binary
*
* @param string $src
* @param bool $strictPadding
* @return string
*/
public static function decode($src, $strictPadding = \false)
{
return static::doDecode($src, \false, $strictPadding);
}
/**
* Decode an uppercase Base32-encoded string into raw binary
*
* @param string $src
* @param bool $strictPadding
* @return string
*/
public static function decodeUpper($src, $strictPadding = \false)
{
return static::doDecode($src, \true, $strictPadding);
}
/**
* Encode into Base32 (RFC 4648)
*
* @param string $src
* @return string
*/
public static function encode($src)
{
return static::doEncode($src, \false);
}
/**
* Encode into Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws \TypeError
*/
public static function encodeUnpadded($src)
{
return static::doEncode($src, false, false);
}
/**
* Encode into uppercase Base32 (RFC 4648)
*
* @param string $src
* @return string
*/
public static function encodeUpper($src)
{
return static::doEncode($src, \true);
}
/**
* Encode into uppercase Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws \TypeError
*/
public static function encodeUpperUnpadded($src)
{
return static::doEncode($src, true, false);
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
*/
protected static function decode5Bits($src)
{
$ret = -1;
// if ($src > 96 && $src < 123) $ret += $src - 97 + 1; // -64
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96);
// if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
$ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return int
*/
protected static function decode5BitsUpper($src)
{
$ret = -1;
// if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
$ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode5Bits($src)
{
$diff = 0x61;
// if ($src > 25) $ret -= 72;
$diff -= ((25 - $src) >> 8) & 73;
return \pack('C', $src + $diff);
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return string
*/
protected static function encode5BitsUpper($src)
{
$diff = 0x41;
// if ($src > 25) $ret -= 40;
$diff -= ((25 - $src) >> 8) & 41;
return \pack('C', $src + $diff);
}
/**
* Base32 decoding
*
* @param string $src
* @param bool $upper
* @param bool $strictPadding
* @return string
*/
protected static function doDecode($src, $upper = \false, $strictPadding = \true)
{
// We do this to reduce code duplication:
$method = $upper
? 'decode5BitsUpper'
: 'decode5Bits';
// Remove padding
$srcLen = Binary::safeStrlen($src);
if ($srcLen === 0) {
return '';
}
if ($strictPadding) {
if (($srcLen & 7) === 0) {
for ($j = 0; $j < 7; ++$j) {
if ($src[$srcLen - 1] === '=') {
$srcLen--;
} else {
break;
}
}
}
if (($srcLen & 7) === 1) {
throw new \RangeException(
'Incorrect padding'
);
}
} else {
$src = \rtrim($src, '=');
$srcLen = Binary::safeStrlen($src);
}
$err = 0;
$dest = '';
// Main loop (no padding):
for ($i = 0; $i + 8 <= $srcLen; $i += 8) {
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8));
$c0 = static::$method($chunk[1]);
$c1 = static::$method($chunk[2]);
$c2 = static::$method($chunk[3]);
$c3 = static::$method($chunk[4]);
$c4 = static::$method($chunk[5]);
$c5 = static::$method($chunk[6]);
$c6 = static::$method($chunk[7]);
$c7 = static::$method($chunk[8]);
$dest .= \pack(
'CCCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff,
(($c6 << 5) | ($c7 ) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8;
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
$c0 = static::$method($chunk[1]);
if ($i + 6 < $srcLen) {
$c1 = static::$method($chunk[2]);
$c2 = static::$method($chunk[3]);
$c3 = static::$method($chunk[4]);
$c4 = static::$method($chunk[5]);
$c5 = static::$method($chunk[6]);
$c6 = static::$method($chunk[7]);
$dest .= \pack(
'CCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8;
} elseif ($i + 5 < $srcLen) {
$c1 = static::$method($chunk[2]);
$c2 = static::$method($chunk[3]);
$c3 = static::$method($chunk[4]);
$c4 = static::$method($chunk[5]);
$c5 = static::$method($chunk[6]);
$dest .= \pack(
'CCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8;
} elseif ($i + 4 < $srcLen) {
$c1 = static::$method($chunk[2]);
$c2 = static::$method($chunk[3]);
$c3 = static::$method($chunk[4]);
$c4 = static::$method($chunk[5]);
$dest .= \pack(
'CCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8;
} elseif ($i + 3 < $srcLen) {
$c1 = static::$method($chunk[2]);
$c2 = static::$method($chunk[3]);
$c3 = static::$method($chunk[4]);
$dest .= \pack(
'CC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3) >> 8;
} elseif ($i + 2 < $srcLen) {
$c1 = static::$method($chunk[2]);
$c2 = static::$method($chunk[3]);
$dest .= \pack(
'CC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) ) & 0xff
);
$err |= ($c0 | $c1 | $c2) >> 8;
} elseif ($i + 1 < $srcLen) {
$c1 = static::$method($chunk[2]);
$dest .= \pack(
'C',
(($c0 << 3) | ($c1 >> 2) ) & 0xff
);
$err |= ($c0 | $c1) >> 8;
} else {
$dest .= \pack(
'C',
(($c0 << 3) ) & 0xff
);
$err |= ($c0) >> 8;
}
}
if ($err !== 0) {
throw new \RangeException(
'Base32::doDecode() only expects characters in the correct base32 alphabet'
);
}
return $dest;
}
/**
* Base32 Decoding
*
* @param string $src
* @param bool $upper
* @param bool $pad
* @return string
*/
protected static function doEncode($src, $upper = \false, $pad = \true)
{
// We do this to reduce code duplication:
$method = $upper
? 'encode5BitsUpper'
: 'encode5Bits';
$dest = '';
$srcLen = Binary::safeStrlen($src);
// Main loop (no padding):
for ($i = 0; $i + 5 <= $srcLen; $i += 5) {
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5));
$b0 = $chunk[1];
$b1 = $chunk[2];
$b2 = $chunk[3];
$b3 = $chunk[4];
$b4 = $chunk[5];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
static::$method((($b3 >> 2) ) & 31) .
static::$method((($b3 << 3) | ($b4 >> 5)) & 31) .
static::$method( $b4 & 31);
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
$b0 = $chunk[1];
if ($i + 3 < $srcLen) {
$b1 = $chunk[2];
$b2 = $chunk[3];
$b3 = $chunk[4];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
static::$method((($b3 >> 2) ) & 31) .
static::$method((($b3 << 3) ) & 31);
if ($pad) {
$dest .= '=';
}
} elseif ($i + 2 < $srcLen) {
$b1 = $chunk[2];
$b2 = $chunk[3];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) ) & 31);
if ($pad) {
$dest .= '===';
}
} elseif ($i + 1 < $srcLen) {
$b1 = $chunk[2];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) ) & 31);
if ($pad) {
$dest .= '====';
}
} else {
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method( ($b0 << 2) & 31);
if ($pad) {
$dest .= '======';
}
}
}
return $dest;
}
}
@@ -0,0 +1,97 @@
<?php
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2017 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Binary
*
* Binary string operators that don't choke on
* mbstring.func_overload
*
* @package ParagonIE\ConstantTime
*/
abstract class Binary
{
/**
* Safe string length
*
* @ref mbstring.func_overload
*
* @param string $str
* @return int
*/
public static function safeStrlen($str)
{
if (\function_exists('mb_strlen')) {
return (int) \mb_strlen($str, '8bit');
} else {
return (int) \strlen($str);
}
}
/**
* Safe substring
*
* @ref mbstring.func_overload
*
* @staticvar boolean $exists
* @param string $str
* @param int $start
* @param int $length
* @return string
* @throws \TypeError
*/
public static function safeSubstr(
$str,
$start = 0,
$length = \null
) {
if (\function_exists('mb_substr')) {
// mb_substr($str, 0, null, '8bit') returns an empty string on PHP
// 5.3, so we have to find the length ourselves.
if (\is_null($length)) {
if ($start >= 0) {
$length = self::safeStrlen($str) - $start;
} else {
$length = -$start;
}
}
// $length calculation above might result in a 0-length string
if ($length === 0) {
return '';
}
return \mb_substr($str, $start, $length, '8bit');
}
if ($length === 0) {
return '';
}
// Unlike mb_substr(), substr() doesn't accept null for length
if (!is_null($length)) {
return \substr($str, $start, $length);
} else {
return \substr($str, $start);
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2017 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Interface EncoderInterface
* @package ParagonIE\ConstantTime
*/
interface EncoderInterface
{
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
*/
public static function encode($bin_string);
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $encoded_string
* @return string (raw binary)
*/
public static function decode($encoded_string);
}

Some files were not shown because too many files have changed in this diff Show More