moodle/admin/tool/mfa/classes/local/secret_manager.php
Stevani Andolo a639783409 MDL-78509 tool_mfa: Boarded the tool_mfa into core
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>
2023-08-29 11:06:44 +08:00

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]);
}
}