mirror of
https://github.com/moodle/moodle.git
synced 2025-08-11 03:46:42 +02:00

In this patch the "commitid" of the "upstream" has been included in this commit. Also, we did not include three factors, which are SMS, Security Questions and Login Banner. Co-authored-by: Peter Burnett <peterburnett@catalyst-au.net> Co-authored-by: Misha Golenkov <golenkovm@gmail.com> Co-authored-by: Brendan Heywood <brendan@catalyst-au.net> Co-authored-by: Alex Morris <alex.morris@catalyst.net.nz> Co-authored-by: Dan Marsden <dan@danmarsden.com> Co-authored-by: Kevin Pham <kevinpham@catalyst-au.net> Co-authored-by: Chris Pratt <tonyyeb@gmail.com> Co-authored-by: Andrew Lyons <andrew@nicols.co.uk> Co-authored-by: Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> Co-authored-by: Liam Kearney <https://github.com/LiamKearn> Co-authored-by: Nicholas Hoobin <nicholashoobin@catalyst-au.net> Co-authored-by: Scott Verbeek <scottverbeek@catalyst-au.net> Co-authored-by: nomisge <https://github.com/nomisge> Co-authored-by: Dmitrii Metelkin <dmitriim@catalyst-au.net> Co-authored-by: Matthew Hilton <matthewhilton@catalyst-au.net> Co-authored-by: Michael Geering <https://github.com/dryj> Co-authored-by: alphadijkstra <https://github.com/alphadijkstra>
244 lines
7.6 KiB
PHP
244 lines
7.6 KiB
PHP
<?php
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
namespace tool_mfa\local;
|
|
|
|
/**
|
|
* MFA secret management class.
|
|
*
|
|
* @package tool_mfa
|
|
* @author Peter Burnett <peterburnett@catalyst-au.net>
|
|
* @copyright Catalyst IT
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
class secret_manager {
|
|
|
|
/** @var string */
|
|
const REVOKED = 'revoked';
|
|
|
|
/** @var string */
|
|
const VALID = 'valid';
|
|
|
|
/** @var string */
|
|
const NONVALID = 'nonvalid';
|
|
|
|
/** @var string */
|
|
private $factor;
|
|
|
|
/** @var string|false */
|
|
private $sessionid;
|
|
|
|
/**
|
|
* Initialises a secret manager instance
|
|
*
|
|
* @param string $factor
|
|
*/
|
|
public function __construct(string $factor) {
|
|
$this->factor = $factor;
|
|
$this->sessionid = session_id();
|
|
}
|
|
|
|
/**
|
|
* This function creates or takes a secret, and stores it in the database or session.
|
|
*
|
|
* @param int $expires the length of time the secret is valid. e.g. 1 min = 60
|
|
* @param bool $session whether this secret should be linked to the session.
|
|
* @param string $secret an optional provided secret
|
|
* @return string the secret code, or 0 if no new code created.
|
|
*/
|
|
public function create_secret(int $expires, bool $session, string $secret = null): string {
|
|
// Check if there already an active secret, unless we are forcibly given a code.
|
|
if ($this->has_active_secret($session) && empty($secret)) {
|
|
return '';
|
|
}
|
|
|
|
// Setup a secret if not provided.
|
|
if (empty($secret)) {
|
|
$secret = random_int(100000, 999999);
|
|
}
|
|
|
|
// Now pass the code where it needs to go.
|
|
if ($session) {
|
|
$this->add_secret_to_db($secret, $expires, $this->sessionid);
|
|
} else {
|
|
$this->add_secret_to_db($secret, $expires);
|
|
}
|
|
|
|
return $secret;
|
|
}
|
|
|
|
/**
|
|
* Inserts the provided secret into the database with a given expiry duration.
|
|
*
|
|
* @param string $secret the secret to store
|
|
* @param int $expires expiry duration in seconds
|
|
* @param string $sessionid an optional sessionID to tie this record to
|
|
* @return void
|
|
*/
|
|
private function add_secret_to_db(string $secret, int $expires, string $sessionid = null) {
|
|
global $DB, $USER;
|
|
$expirytime = time() + $expires;
|
|
|
|
$data = [
|
|
'userid' => $USER->id,
|
|
'factor' => $this->factor,
|
|
'secret' => $secret,
|
|
'timecreated' => time(),
|
|
'expiry' => $expirytime,
|
|
'revoked' => 0,
|
|
];
|
|
if (!empty($sessionid)) {
|
|
$data['sessionid'] = $sessionid;
|
|
}
|
|
$DB->insert_record('tool_mfa_secrets', $data);
|
|
}
|
|
|
|
/**
|
|
* Validates whether the provided secret is currently valid.
|
|
*
|
|
* @param string $secret the secret to check
|
|
* @param bool $keep should the secret be kept for reuse until expiry?
|
|
* @return string a secret manager state constant
|
|
*/
|
|
public function validate_secret(string $secret, bool $keep = false): string {
|
|
global $DB, $USER;
|
|
$status = $this->check_secret_against_db($secret, $this->sessionid);
|
|
if ($status !== self::NONVALID) {
|
|
if ($status === self::VALID && !$keep) {
|
|
// Cleanup DB $record.
|
|
$DB->delete_records('tool_mfa_secrets', ['userid' => $USER->id, 'factor' => $this->factor]);
|
|
}
|
|
return $status;
|
|
}
|
|
// This is always nonvalid.
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Checks if a given secret is valid from the Database.
|
|
*
|
|
* @param string $secret the secret to check.
|
|
* @param string $sessionid the session id to check for.
|
|
* @return string a secret manager state constant.
|
|
*/
|
|
private function check_secret_against_db(string $secret, string $sessionid): string {
|
|
global $DB, $USER;
|
|
|
|
$sql = "SELECT *
|
|
FROM {tool_mfa_secrets}
|
|
WHERE secret = :secret
|
|
AND expiry > :now
|
|
AND userid = :userid
|
|
AND factor = :factor";
|
|
|
|
$params = [
|
|
'secret' => $secret,
|
|
'now' => time(),
|
|
'userid' => $USER->id,
|
|
'factor' => $this->factor,
|
|
];
|
|
|
|
$record = $DB->get_record_sql($sql, $params);
|
|
|
|
if (!empty($record)) {
|
|
// If revoked it should always be revoked status.
|
|
if ($record->revoked) {
|
|
return self::REVOKED;
|
|
}
|
|
|
|
// Check if this is valid in only one session.
|
|
if (!empty($record->sessionid)) {
|
|
if ($record->sessionid === $sessionid) {
|
|
return self::VALID;
|
|
}
|
|
return self::NONVALID;
|
|
}
|
|
return self::VALID;
|
|
}
|
|
return self::NONVALID;
|
|
}
|
|
|
|
/**
|
|
* Revokes the provided secret code for the user.
|
|
*
|
|
* @param string $secret the secret to revoke.
|
|
* @param int $userid the userid to revoke the secret for.
|
|
* @return void
|
|
*/
|
|
public function revoke_secret(string $secret, $userid = null) {
|
|
global $DB, $USER;
|
|
|
|
$userid = $userid ?? $USER->id;
|
|
|
|
// We do not need to worry about session vs global here.
|
|
// A factor should only ever use one.
|
|
// We know this secret is valid, so we don't need to check expiry.
|
|
$DB->set_field('tool_mfa_secrets', 'revoked', 1, ['userid' => $userid, 'factor' => $this->factor, 'secret' => $secret]);
|
|
}
|
|
|
|
/**
|
|
* Checks whether this factor currently has an active secret, and should not add another.
|
|
*
|
|
* @param bool $checksession should we only check if a current session secret is active?
|
|
* @return bool
|
|
*/
|
|
private function has_active_secret(bool $checksession = false): bool {
|
|
global $DB, $USER;
|
|
|
|
$sql = "SELECT *
|
|
FROM {tool_mfa_secrets}
|
|
WHERE expiry > :now
|
|
AND userid = :userid
|
|
AND factor = :factor
|
|
AND revoked = 0";
|
|
|
|
$params = [
|
|
'now' => time(),
|
|
'userid' => $USER->id,
|
|
'factor' => $this->factor,
|
|
];
|
|
|
|
if ($checksession) {
|
|
$sql .= ' AND sessionid = :sessionid';
|
|
$params['sessionid'] = $this->sessionid;
|
|
}
|
|
|
|
if ($DB->record_exists_sql($sql, $params)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Deletes any user secrets hanging around in the database.
|
|
*
|
|
* @param int $userid the userid to cleanup temp secrets for.
|
|
* @return void
|
|
*/
|
|
public function cleanup_temp_secrets($userid = null) {
|
|
global $DB, $USER;
|
|
// Session records are autocleaned up.
|
|
// Only DB cleanup required.
|
|
|
|
$userid = $userid ?? $USER->id;
|
|
$sql = 'DELETE FROM {tool_mfa_secrets}
|
|
WHERE userid = :userid
|
|
AND factor = :factor';
|
|
|
|
$DB->execute($sql, ['userid' => $userid, 'factor' => $this->factor]);
|
|
}
|
|
}
|