1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace factor_token; 18 19 use stdClass; 20 use tool_mfa\local\factor\object_factor_base; 21 use tool_mfa\local\secret_manager; 22 23 /** 24 * Token factor class. 25 * 26 * @package factor_token 27 * @author Peter Burnett <peterburnett@catalyst-au.net> 28 * @copyright Catalyst IT 29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 */ 31 class factor extends object_factor_base { 32 33 /** 34 * Token implementation. 35 * 36 * {@inheritDoc} 37 */ 38 public function has_input(): bool { 39 return false; 40 } 41 42 /** 43 * Token implementation. 44 * This factor is a singleton, return single instance. 45 * 46 * @param stdClass $user the user to check against. 47 * @return array 48 */ 49 public function get_all_user_factors(stdClass $user): array { 50 global $DB; 51 $records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]); 52 53 if (!empty($records)) { 54 return $records; 55 } 56 57 // Null records returned, build new record. 58 $record = [ 59 'userid' => $user->id, 60 'factor' => $this->name, 61 'timecreated' => time(), 62 'createdfromip' => $user->lastip, 63 'timemodified' => time(), 64 'revoked' => 0, 65 ]; 66 $record['id'] = $DB->insert_record('tool_mfa', $record, true); 67 return [(object) $record]; 68 } 69 70 /** 71 * Token implementation. 72 * Checks whether the user has selected roles in any context. 73 * 74 * {@inheritDoc} 75 */ 76 public function get_state(): string { 77 global $USER; 78 79 // Check if there was a previous locked status to return. 80 $state = parent::get_state(); 81 if ($state === \tool_mfa\plugininfo\factor::STATE_LOCKED) { 82 return \tool_mfa\plugininfo\factor::STATE_LOCKED; 83 } 84 85 // Check cookie Exists. 86 $cookie = 'MFA_TOKEN_' . $USER->id; 87 if (NO_MOODLE_COOKIES || empty($_COOKIE[$cookie])) { 88 return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; 89 } 90 $token = $_COOKIE[$cookie]; 91 92 $secretmanager = new secret_manager($this->name); 93 $verified = $secretmanager->validate_secret($token, true); 94 95 // If we got a bad cookie value, someone is likely being dodgy. 96 // In this instance we should just lock and make the user re-MFA. 97 if ($verified === secret_manager::NONVALID) { 98 $this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED); 99 return \tool_mfa\plugininfo\factor::STATE_LOCKED; 100 } else if ($verified === secret_manager::VALID) { 101 return \tool_mfa\plugininfo\factor::STATE_PASS; 102 } 103 104 // We should never get here. Factor cannot be revoked. 105 return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; 106 } 107 108 /** 109 * Token Implementation. 110 * We can't get_state like the parent here or it will recurse forever. 111 * 112 * @param string $state the state constant to set 113 * @return bool 114 */ 115 public function set_state($state): bool { 116 global $SESSION; 117 $property = 'factor_' . $this->name; 118 $SESSION->$property = $state; 119 return true; 120 } 121 122 /** 123 * Token implementation. 124 * 125 * @param stdClass $user 126 * @return array 127 */ 128 public function possible_states(stdClass $user): array { 129 return [ 130 \tool_mfa\plugininfo\factor::STATE_PASS, 131 \tool_mfa\plugininfo\factor::STATE_NEUTRAL, 132 \tool_mfa\plugininfo\factor::STATE_LOCKED, 133 ]; 134 } 135 136 /** 137 * Token implementation. 138 * Inject a checkbox into every auth form if needed. 139 * 140 * @param \MoodleQuickForm $mform Form to inject global elements into. 141 * @return void 142 */ 143 public function global_definition_after_data($mform): void { 144 global $SESSION; 145 146 // First thing, we need to decide on whether we should show the checkbox. 147 $noproperty = !property_exists($SESSION, 'tool_mfa_factor_token'); 148 $nostate = $this->get_state() !== \tool_mfa\plugininfo\factor::STATE_PASS; 149 150 if ($noproperty && $nostate) { 151 $expiry = get_config('factor_token', 'expiry'); 152 $expirystring = format_time($expiry); 153 $mform->addElement('advcheckbox', 'factor_token_trust', '', get_string('form:trust', 'factor_token', $expirystring)); 154 $mform->setType('factor_token_trust', PARAM_BOOL); 155 $mform->setDefault('factor_token_trust', true); 156 } 157 } 158 159 /** 160 * Token implementation. 161 * Store information about the token status. 162 * 163 * @param object $data Data from the form. 164 * @return void 165 */ 166 public function global_submit($data): void { 167 global $SESSION; 168 169 // Store any kind of response here, we shouldnt show again. 170 $trust = $data->factor_token_trust; 171 $SESSION->tool_mfa_factor_token = $trust; 172 } 173 174 /** 175 * Token implementation. 176 * Pass hook to set the cookie for use in subsequent auths. 177 * 178 * {@inheritDoc} 179 */ 180 public function post_pass_state(): void { 181 global $CFG, $SESSION, $USER; 182 183 if (!property_exists($SESSION, 'tool_mfa_factor_token')) { 184 return; 185 } 186 $settoken = $SESSION->tool_mfa_factor_token; 187 if (!$settoken) { 188 return; 189 } 190 $cookie = 'MFA_TOKEN_' . $USER->id; 191 192 list($expirytime, $expiry) = $this->calculate_expiry_time(); 193 194 // Store this secret in the database. 195 $secretmanager = new secret_manager($this->name); 196 $secret = base64_encode(random_bytes(256)); 197 $secretmanager->create_secret($expiry, false, $secret); 198 199 // All the prep is now done, we can set this cookie. 200 setcookie($cookie, $secret, $expirytime, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, false, true); 201 202 // Finally emit a log event for storing the cookie. 203 $state = [ 204 'expiry' => $expirytime, 205 'cookie' => $cookie, 206 ]; 207 $event = \factor_token\event\token_created::token_created_event($USER, $state); 208 $event->trigger(); 209 } 210 211 /** 212 * Calculate the expiry time of the token, based on configuration. 213 * 214 * @param integer|null $basetime time to use for calcalations. 215 * @return array 216 */ 217 public function calculate_expiry_time($basetime = null): array { 218 if (empty($basetime)) { 219 $basetime = time(); 220 } 221 222 // Calculate the expiry time. This is provided by config, 223 // But optionally might need to be rounded to expire a few hours after 0000 server time. 224 $expiry = get_config('factor_token', 'expiry'); 225 $expirytime = $basetime + $expiry; 226 227 // If expiring overnight, it should expire at 2am the following morning, if required. 228 $expireovernight = get_config('factor_token', 'expireovernight'); 229 if ($expireovernight) { 230 // Find out what 2am the following morning time is. 231 $datetime = new \DateTime(); 232 $timezone = \core_date::get_user_timezone_object(); 233 234 // Bit to ensure 'expireovernight' works when 'expire' is longer than one day. 235 $difftime = 0; 236 if ($expiry > DAYSECS) { 237 // Ensures a safe amount of days is added before doing the 2am checks. 238 $difftime = $expiry - DAYSECS; 239 } 240 241 // Calculte the overnight expiry time, ignoring 'expiry' duration period. 242 $workingexpirytime = $basetime + $difftime; 243 $datetime->setTimezone($timezone); 244 $datetime->setTimestamp($workingexpirytime); 245 $datetime->add(new \DateInterval('P1D')); 246 $datetime->setTime(2, 0); // Set the hour to 2am. 247 248 // Ensure whatever happens, ensure the expiry never goes over the default 'expiry' time. 249 $overnightexpirytime = $datetime->getTimestamp(); 250 $expirytime = min($overnightexpirytime, $expirytime); 251 $expiry = $expirytime - $basetime; 252 } 253 254 return [$expirytime, $expiry]; 255 } 256 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body