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_webauthn; 18 19 defined('MOODLE_INTERNAL') || die(); 20 21 require_once($CFG->libdir . '/webauthn/src/WebAuthn.php'); 22 23 use lbuchs\WebAuthn\Binary\ByteBuffer; 24 use lbuchs\WebAuthn\WebAuthn; 25 use lbuchs\WebAuthn\WebAuthnException; 26 use stdClass; 27 use tool_mfa\local\factor\object_factor_base; 28 29 /** 30 * WebAuthn factor class. 31 * 32 * @package factor_webauthn 33 * @author Alex Morris <alex.morris@catalyst.net.nz> 34 * @copyright Catalyst IT 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class factor extends object_factor_base { 38 39 /** @var WebAuthn WebAuthn server */ 40 private $webauthn; 41 /** @var string Relying party ID */ 42 private $rpid; 43 /** @var string User verification setting */ 44 private $userverification; 45 46 /** @var string Factor icon */ 47 protected $icon = 'fa-hand-pointer'; 48 49 /** 50 * Create webauthn server. 51 * 52 * @param string $name 53 */ 54 public function __construct($name) { 55 global $CFG, $SITE; 56 parent::__construct($name); 57 58 $this->rpid = (new \moodle_url($CFG->wwwroot))->get_host(); 59 $this->webauthn = new WebAuthn($SITE->fullname, $this->rpid); 60 61 $this->userverification = get_config('factor_webauthn', 'userverification'); 62 } 63 64 /** 65 * WebAuthn Factor implementation. 66 * 67 * @param stdClass $user the user to check against. 68 * @return array 69 */ 70 public function get_all_user_factors(stdClass $user): array { 71 global $DB; 72 return $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]); 73 } 74 75 /** 76 * WebAuthn Factor implementation. 77 * 78 * {@inheritDoc} 79 */ 80 public function has_input(): bool { 81 return true; 82 } 83 84 /** 85 * WebAuthn Factor implementation. 86 * 87 * {@inheritDoc} 88 */ 89 public function has_revoke(): bool { 90 return true; 91 } 92 93 /** 94 * WebAuthn Factor implementation. 95 * 96 * {@inheritDoc} 97 */ 98 public function has_setup(): bool { 99 return true; 100 } 101 102 /** 103 * WebAuthn Factor implementation. 104 * 105 * {@inheritDoc} 106 */ 107 public function show_setup_buttons(): bool { 108 return true; 109 } 110 111 /** 112 * WebAuthn factor implementation. 113 * 114 * @param stdClass $user 115 * @return array 116 */ 117 public function possible_states(stdClass $user): array { 118 return [ 119 \tool_mfa\plugininfo\factor::STATE_PASS, 120 \tool_mfa\plugininfo\factor::STATE_NEUTRAL, 121 \tool_mfa\plugininfo\factor::STATE_UNKNOWN, 122 ]; 123 } 124 125 /** 126 * WebAuthn state 127 * 128 * {@inheritDoc} 129 */ 130 public function get_state(): string { 131 global $USER; 132 $userfactors = $this->get_active_user_factors($USER); 133 134 // If no authenticators are set up then we are neutral not unknown. 135 if (count($userfactors) == 0) { 136 return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; 137 } 138 139 return parent::get_state(); 140 } 141 142 /** 143 * Gets the string for setup button on preferences page. 144 * 145 * @return string 146 */ 147 public function get_setup_string(): string { 148 return get_string('setupfactor', 'factor_webauthn'); 149 } 150 151 /** 152 * WebAuthn Factor implementation. 153 * 154 * @param \MoodleQuickForm $mform 155 * @return \MoodleQuickForm $mform 156 */ 157 public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm { 158 global $PAGE, $USER, $SESSION; 159 160 $mform->addElement('hidden', 'response_input', '', ['id' => 'id_response_input']); 161 $mform->setType('response_input', PARAM_RAW); 162 163 // Required to attach verification errors, so they can be displayed to the user. 164 $mform->addElement('static', 'verificationcode', '', ''); 165 166 $ids = []; 167 168 $authenticators = $this->get_active_user_factors($USER); 169 foreach ($authenticators as $authenticator) { 170 $registration = json_decode($authenticator->secret); 171 $ids[] = base64_decode($registration->credentialId); 172 } 173 174 $types = explode(',', get_config('factor_webauthn', 'authenticatortypes')); 175 $getargs = 176 $this->webauthn->getGetArgs($ids, 20, in_array('usb', $types), in_array('nfc', $types), in_array('ble', $types), 177 in_array('hybrid', $types), in_array('internal', $types), $this->userverification); 178 179 $PAGE->requires->js_call_amd('factor_webauthn/login', 'init', [json_encode($getargs)]); 180 181 // Challenge is regenerated on form submission, at this point we aren't aware if the form is submitted for being 182 // loaded for the first time, so we store the existing and new challenge. 183 if (isset($SESSION->factor_webauthn_challenge_new)) { 184 $SESSION->factor_webauthn_challenge = $SESSION->factor_webauthn_challenge_new; 185 } 186 $SESSION->factor_webauthn_challenge_new = $this->webauthn->getChallenge()->getHex(); 187 188 return $mform; 189 } 190 191 /** 192 * WebAuthn Factor implementation. 193 * 194 * @param array $data 195 * @return array 196 */ 197 public function login_form_validation(array $data): array { 198 global $USER, $SESSION; 199 200 $errors = []; 201 if (empty($data['response_input'])) { 202 $errors['verificationcode'] = get_string('error', 'factor_webauthn'); 203 return $errors; 204 } 205 206 $post = json_decode($data['response_input'], null, 512, JSON_THROW_ON_ERROR); 207 208 $id = base64_decode($post->id); 209 $clientdata = base64_decode($post->clientDataJSON); 210 $authenticatordata = base64_decode($post->authenticatorData); 211 $signature = base64_decode($post->signature); 212 $credentialpublickey = null; 213 $challenge = ByteBuffer::fromHex($SESSION->factor_webauthn_challenge); 214 unset($SESSION->factor_webauthn_challenge); 215 216 $authenticators = $this->get_active_user_factors($USER); 217 foreach ($authenticators as $authenticator) { 218 $registration = json_decode($authenticator->secret); 219 if (base64_decode($registration->credentialId) === $id) { 220 $credentialpublickey = $registration->credentialPublicKey; 221 break; 222 } 223 } 224 225 if ($credentialpublickey === null) { 226 $errors['verificationcode'] = get_string('error', 'factor_webauthn'); 227 return $errors; 228 } 229 230 try { 231 // Throws exception if authentication fails. 232 $this->webauthn->processGet($clientdata, $authenticatordata, $signature, $credentialpublickey, $challenge, null, 233 $this->userverification === 'required'); 234 } catch (WebAuthnException $ex) { 235 $errors['verificationcode'] = get_string('error', 'factor_webauthn'); 236 } 237 238 return $errors; 239 } 240 241 /** 242 * WebAuthn Factor implementation. 243 * 244 * @param \MoodleQuickForm $mform 245 * @return object $mform 246 */ 247 public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm { 248 global $PAGE, $USER, $SESSION; 249 250 $mform->addElement('text', 'webauthn_name', get_string('authenticatorname', 'factor_webauthn')); 251 $mform->setType('webauthn_name', PARAM_ALPHANUM); 252 $mform->addRule('webauthn_name', get_string('required'), 'required', null, 'client'); 253 254 $registerbtn = \html_writer::tag('btn', get_string('register', 'factor_webauthn'), [ 255 'class' => 'btn btn-primary', 256 'type' => 'button', 257 'id' => 'factor_webauthn-register' 258 ]); 259 $mform->addElement('static', 'register', '', $registerbtn); 260 261 $mform->addElement('hidden', 'response_input', '', ['id' => 'id_response_input']); 262 $mform->setType('response_input', PARAM_RAW); 263 $mform->addRule('response_input', get_string('required'), 'required', null, 'client'); 264 265 // Cross-platform: true if type internal is not allowed, 266 // false if only internal is allowed, 267 // null if internal and cross-platform is allowed. 268 $types = explode(',', get_config('factor_webauthn', 'authenticatortypes')); 269 $crossplatformattachment = null; 270 if ((in_array('usb', $types) || in_array('nfc', $types) || in_array('ble', $types) || in_array('hybrid', $types)) && 271 !in_array('internal', $types)) { 272 $crossplatformattachment = true; 273 } else if (!in_array('usb', $types) && !in_array('nfc', $types) && !in_array('ble', $types) && 274 !in_array('hybrid', $types) && in_array('internal', $types)) { 275 $crossplatformattachment = false; 276 } 277 278 $createargs = $this->webauthn->getCreateArgs($USER->id, $USER->username, fullname($USER), 20, false, 279 $this->userverification, $crossplatformattachment); 280 281 $PAGE->requires->js_call_amd('factor_webauthn/register', 'init', [json_encode($createargs)]); 282 283 // Challenge is regenerated on form submission, at this point we aren't aware if the form is submitted for being 284 // loaded for the first time, so we store the existing and new challenge. 285 if (isset($SESSION->factor_webauthn_challenge_new)) { 286 $SESSION->factor_webauthn_challenge = $SESSION->factor_webauthn_challenge_new; 287 } 288 $SESSION->factor_webauthn_challenge_new = $this->webauthn->getChallenge()->getHex(); 289 290 return $mform; 291 } 292 293 /** 294 * WebAuthn Factor implementation. 295 * 296 * @param object $data 297 * @return stdClass|null 298 */ 299 public function setup_user_factor(object $data): stdClass|null { 300 global $DB, $USER, $SESSION; 301 302 if (!empty($data->webauthn_name) && !empty($data->response_input) && isset($SESSION->factor_webauthn_challenge)) { 303 $post = json_decode($data->response_input, null, 512, JSON_THROW_ON_ERROR); 304 305 $clientdata = base64_decode($post->clientDataJSON); 306 $attestationobject = base64_decode($post->attestationObject); 307 $challenge = ByteBuffer::fromHex($SESSION->factor_webauthn_challenge); 308 unset($SESSION->factor_webauthn_challenge); 309 310 $registration = 311 $this->webauthn->processCreate($clientdata, $attestationobject, $challenge, $this->userverification === 'required', 312 true, false); 313 $registration->credentialId = base64_encode($registration->credentialId); 314 $registration->AAGUID = base64_encode($registration->AAGUID); 315 unset($registration->certificate); 316 317 $row = new \stdClass(); 318 $row->userid = $USER->id; 319 $row->factor = $this->name; 320 $row->label = $data->webauthn_name; 321 $row->secret = json_encode($registration); 322 $row->timecreated = time(); 323 $row->createdfromip = $USER->lastip; 324 $row->timemodified = time(); 325 $row->lastverified = time(); 326 $row->revoked = 0; 327 328 $id = $DB->insert_record('tool_mfa', $row); 329 330 $record = $DB->get_record('tool_mfa', array('id' => $id)); 331 $this->create_event_after_factor_setup($USER); 332 333 return $record; 334 } 335 return null; 336 } 337 338 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body