See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 /** 18 * Anobody can login with any password. 19 * 20 * @package auth_oauth2 21 * @copyright 2017 Damyon Wiese 22 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 23 */ 24 25 namespace auth_oauth2; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 use pix_icon; 30 use moodle_url; 31 use core_text; 32 use context_system; 33 use stdClass; 34 use core\oauth2\issuer; 35 use core\oauth2\client; 36 37 require_once($CFG->libdir.'/authlib.php'); 38 require_once($CFG->dirroot.'/user/lib.php'); 39 require_once($CFG->dirroot.'/user/profile/lib.php'); 40 41 /** 42 * Plugin for oauth2 authentication. 43 * 44 * @package auth_oauth2 45 * @copyright 2017 Damyon Wiese 46 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License 47 */ 48 class auth extends \auth_plugin_base { 49 50 /** 51 * @var stdClass $userinfo The set of user info returned from the oauth handshake 52 */ 53 private static $userinfo; 54 55 /** 56 * @var stdClass $userpicture The url to a picture. 57 */ 58 private static $userpicture; 59 60 /** 61 * Constructor. 62 */ 63 public function __construct() { 64 $this->authtype = 'oauth2'; 65 $this->config = get_config('auth_oauth2'); 66 $this->customfields = $this->get_custom_user_profile_fields(); 67 } 68 69 /** 70 * Returns true if the username and password work or don't exist and false 71 * if the user exists and the password is wrong. 72 * 73 * @param string $username The username 74 * @param string $password The password 75 * @return bool Authentication success or failure. 76 */ 77 public function user_login($username, $password) { 78 $cached = $this->get_static_user_info(); 79 if (empty($cached)) { 80 // This means we were called as part of a normal login flow - without using oauth. 81 return false; 82 } 83 $verifyusername = $cached['username']; 84 if ($verifyusername == $username) { 85 return true; 86 } 87 return false; 88 } 89 90 /** 91 * We don't want to allow users setting an internal password. 92 * 93 * @return bool 94 */ 95 public function prevent_local_passwords() { 96 return true; 97 } 98 99 /** 100 * Returns true if this authentication plugin is 'internal'. 101 * 102 * @return bool 103 */ 104 public function is_internal() { 105 return false; 106 } 107 108 /** 109 * Indicates if moodle should automatically update internal user 110 * records with data from external sources using the information 111 * from auth_plugin_base::get_userinfo(). 112 * 113 * @return bool true means automatically copy data from ext to user table 114 */ 115 public function is_synchronised_with_external() { 116 return true; 117 } 118 119 /** 120 * Returns true if this authentication plugin can change the user's 121 * password. 122 * 123 * @return bool 124 */ 125 public function can_change_password() { 126 return false; 127 } 128 129 /** 130 * Returns the URL for changing the user's pw, or empty if the default can 131 * be used. 132 * 133 * @return moodle_url 134 */ 135 public function change_password_url() { 136 return null; 137 } 138 139 /** 140 * Returns true if plugin allows resetting of internal password. 141 * 142 * @return bool 143 */ 144 public function can_reset_password() { 145 return false; 146 } 147 148 /** 149 * Returns true if plugin can be manually set. 150 * 151 * @return bool 152 */ 153 public function can_be_manually_set() { 154 return true; 155 } 156 157 /** 158 * Return the userinfo from the oauth handshake. Will only be valid 159 * for the logged in user. 160 * @param string $username 161 */ 162 public function get_userinfo($username) { 163 $cached = $this->get_static_user_info(); 164 if (!empty($cached) && $cached['username'] == $username) { 165 return $cached; 166 } 167 return false; 168 } 169 170 /** 171 * Return a list of identity providers to display on the login page. 172 * 173 * @param string|moodle_url $wantsurl The requested URL. 174 * @return array List of arrays with keys url, iconurl and name. 175 */ 176 public function loginpage_idp_list($wantsurl) { 177 $providers = \core\oauth2\api::get_all_issuers(true); 178 $result = []; 179 if (empty($wantsurl)) { 180 $wantsurl = '/'; 181 } 182 foreach ($providers as $idp) { 183 if ($idp->is_available_for_login()) { 184 $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()]; 185 $url = new moodle_url('/auth/oauth2/login.php', $params); 186 $icon = $idp->get('image'); 187 $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()]; 188 } 189 } 190 return $result; 191 } 192 193 /** 194 * Statically cache the user info from the oauth handshake 195 * @param stdClass $userinfo 196 */ 197 private function set_static_user_info($userinfo) { 198 self::$userinfo = $userinfo; 199 } 200 201 /** 202 * Get the static cached user info 203 * @return stdClass 204 */ 205 private function get_static_user_info() { 206 return self::$userinfo; 207 } 208 209 /** 210 * Statically cache the user picture from the oauth handshake 211 * @param string $userpicture 212 */ 213 private function set_static_user_picture($userpicture) { 214 self::$userpicture = $userpicture; 215 } 216 217 /** 218 * Get the static cached user picture 219 * @return string 220 */ 221 private function get_static_user_picture() { 222 return self::$userpicture; 223 } 224 225 /** 226 * If this user has no picture - but we got one from oauth - set it. 227 * @param stdClass $user 228 * @return boolean True if the image was updated. 229 */ 230 private function update_picture($user) { 231 global $CFG, $DB, $USER; 232 233 require_once($CFG->libdir . '/filelib.php'); 234 require_once($CFG->libdir . '/gdlib.php'); 235 require_once($CFG->dirroot . '/user/lib.php'); 236 237 $fs = get_file_storage(); 238 $userid = $user->id; 239 if (!empty($user->picture)) { 240 return false; 241 } 242 if (!empty($CFG->enablegravatar)) { 243 return false; 244 } 245 246 $picture = $this->get_static_user_picture(); 247 if (empty($picture)) { 248 return false; 249 } 250 251 $context = \context_user::instance($userid, MUST_EXIST); 252 $fs->delete_area_files($context->id, 'user', 'newicon'); 253 254 $filerecord = array( 255 'contextid' => $context->id, 256 'component' => 'user', 257 'filearea' => 'newicon', 258 'itemid' => 0, 259 'filepath' => '/', 260 'filename' => 'image' 261 ); 262 263 try { 264 $fs->create_file_from_string($filerecord, $picture); 265 } catch (\file_exception $e) { 266 return get_string($e->errorcode, $e->module, $e->a); 267 } 268 269 $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); 270 271 // There should only be one. 272 $iconfile = reset($iconfile); 273 274 // Something went wrong while creating temp file - remove the uploaded file. 275 if (!$iconfile = $iconfile->copy_content_to_temp()) { 276 $fs->delete_area_files($context->id, 'user', 'newicon'); 277 return false; 278 } 279 280 // Copy file to temporary location and the send it for processing icon. 281 $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); 282 // Delete temporary file. 283 @unlink($iconfile); 284 // Remove uploaded file. 285 $fs->delete_area_files($context->id, 'user', 'newicon'); 286 // Set the user's picture. 287 $updateuser = new stdClass(); 288 $updateuser->id = $userid; 289 $updateuser->picture = $newpicture; 290 $USER->picture = $newpicture; 291 user_update_user($updateuser); 292 return true; 293 } 294 295 /** 296 * Update user data according to data sent by authorization server. 297 * 298 * @param array $externaldata data from authorization server 299 * @param stdClass $userdata Current data of the user to be updated 300 * @return stdClass The updated user record, or the existing one if there's nothing to be updated. 301 */ 302 private function update_user(array $externaldata, $userdata) { 303 $user = (object) [ 304 'id' => $userdata->id, 305 ]; 306 307 // We can only update if the default authentication type of the user is set to OAuth2 as well. Otherwise, we might mess 308 // up the user data of other users that use different authentication mechanisms (e.g. linked logins). 309 if ($userdata->auth !== $this->authtype) { 310 return $userdata; 311 } 312 313 $allfields = array_merge($this->userfields, $this->customfields); 314 315 // Go through each field from the external data. 316 foreach ($externaldata as $fieldname => $value) { 317 if (!in_array($fieldname, $allfields)) { 318 // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer. 319 continue; 320 } 321 322 $userhasfield = property_exists($userdata, $fieldname); 323 // Find out if it is a profile field. 324 $isprofilefield = strpos($fieldname, 'profile_field_') === 0; 325 $profilefieldname = str_replace('profile_field_', '', $fieldname); 326 $userhasprofilefield = $isprofilefield && array_key_exists($profilefieldname, $userdata->profile); 327 328 // Just in case this field is on the list, but not part of the user data. This shouldn't happen though. 329 if (!($userhasfield || $userhasprofilefield)) { 330 continue; 331 } 332 333 // Get the old value. 334 $oldvalue = $isprofilefield ? (string) $userdata->profile[$profilefieldname] : (string) $userdata->$fieldname; 335 336 // Get the lock configuration of the field. 337 if (!empty($this->config->{'field_lock_' . $fieldname})) { 338 $lockvalue = $this->config->{'field_lock_' . $fieldname}; 339 } else { 340 $lockvalue = 'unlocked'; 341 } 342 343 // We should update fields that meet the following criteria: 344 // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty. 345 // - The value has changed. 346 if ($lockvalue === 'unlocked' || ($lockvalue === 'unlockedifempty' && empty($oldvalue))) { 347 $value = (string)$value; 348 if ($oldvalue !== $value) { 349 $user->$fieldname = $value; 350 } 351 } 352 } 353 // Update the user data. 354 user_update_user($user, false); 355 356 // Save user profile data. 357 profile_save_data($user); 358 359 // Refresh user for $USER variable. 360 return get_complete_user_data('id', $user->id); 361 } 362 363 /** 364 * Confirm the new user as registered. 365 * 366 * @param string $username 367 * @param string $confirmsecret 368 */ 369 public function user_confirm($username, $confirmsecret) { 370 global $DB; 371 $user = get_complete_user_data('username', $username); 372 373 if (!empty($user)) { 374 if ($user->auth != $this->authtype) { 375 return AUTH_CONFIRM_ERROR; 376 377 } else if ($user->secret === $confirmsecret && $user->confirmed) { 378 return AUTH_CONFIRM_ALREADY; 379 380 } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in. 381 $DB->set_field("user", "confirmed", 1, array("id" => $user->id)); 382 return AUTH_CONFIRM_OK; 383 } 384 } else { 385 return AUTH_CONFIRM_ERROR; 386 } 387 } 388 389 /** 390 * Print a page showing that a confirm email was sent with instructions. 391 * 392 * @param string $title 393 * @param string $message 394 */ 395 public function print_confirm_required($title, $message) { 396 global $PAGE, $OUTPUT, $CFG; 397 398 $PAGE->navbar->add($title); 399 $PAGE->set_title($title); 400 $PAGE->set_heading($PAGE->course->fullname); 401 echo $OUTPUT->header(); 402 notice($message, "$CFG->wwwroot/index.php"); 403 } 404 405 /** 406 * Complete the login process after oauth handshake is complete. 407 * @param \core\oauth2\client $client 408 * @param string $redirecturl 409 * @return void Either redirects or throws an exception 410 */ 411 public function complete_login(client $client, $redirecturl) { 412 global $CFG, $SESSION, $PAGE; 413 414 $rawuserinfo = $client->get_raw_userinfo(); 415 $userinfo = $client->get_userinfo(); 416 417 if (!$userinfo) { 418 // Trigger login failed event. 419 $failurereason = AUTH_LOGIN_NOUSER; 420 $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown', 421 'reason' => $failurereason]]); 422 $event->trigger(); 423 424 $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2'); 425 $SESSION->loginerrormsg = $errormsg; 426 $client->log_out(); 427 redirect(new moodle_url('/login/index.php')); 428 } 429 if (empty($userinfo['username']) || empty($userinfo['email'])) { 430 // Trigger login failed event. 431 $failurereason = AUTH_LOGIN_NOUSER; 432 $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown', 433 'reason' => $failurereason]]); 434 $event->trigger(); 435 436 $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2'); 437 $SESSION->loginerrormsg = $errormsg; 438 $client->log_out(); 439 redirect(new moodle_url('/login/index.php')); 440 } 441 442 $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); 443 $oauthemail = $userinfo['email']; 444 445 // Once we get here we have the user info from oauth. 446 $userwasmapped = false; 447 448 // Clean and remember the picture / lang. 449 if (!empty($userinfo['picture'])) { 450 $this->set_static_user_picture($userinfo['picture']); 451 unset($userinfo['picture']); 452 } 453 454 if (!empty($userinfo['lang'])) { 455 $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); 456 if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { 457 unset($userinfo['lang']); 458 } 459 } 460 461 $issuer = $client->get_issuer(); 462 // First we try and find a defined mapping. 463 $linkedlogin = api::match_username_to_user($userinfo['username'], $issuer); 464 465 if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) { 466 $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid')); 467 468 if ($mappeduser && $mappeduser->suspended) { 469 $failurereason = AUTH_LOGIN_SUSPENDED; 470 $event = \core\event\user_login_failed::create([ 471 'userid' => $mappeduser->id, 472 'other' => [ 473 'username' => $userinfo['username'], 474 'reason' => $failurereason 475 ] 476 ]); 477 $event->trigger(); 478 $SESSION->loginerrormsg = get_string('invalidlogin'); 479 $client->log_out(); 480 redirect(new moodle_url('/login/index.php')); 481 } else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) { 482 // Update user fields. 483 $userinfo = $this->update_user($userinfo, $mappeduser); 484 $userwasmapped = true; 485 } else { 486 // Trigger login failed event. 487 $failurereason = AUTH_LOGIN_UNAUTHORISED; 488 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 489 'reason' => $failurereason]]); 490 $event->trigger(); 491 492 $errormsg = get_string('confirmationpending', 'auth_oauth2'); 493 $SESSION->loginerrormsg = $errormsg; 494 $client->log_out(); 495 redirect(new moodle_url('/login/index.php')); 496 } 497 } else if (!empty($linkedlogin)) { 498 // Trigger login failed event. 499 $failurereason = AUTH_LOGIN_UNAUTHORISED; 500 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 501 'reason' => $failurereason]]); 502 $event->trigger(); 503 504 $errormsg = get_string('confirmationpending', 'auth_oauth2'); 505 $SESSION->loginerrormsg = $errormsg; 506 $client->log_out(); 507 redirect(new moodle_url('/login/index.php')); 508 } 509 510 511 if (!$issuer->is_valid_login_domain($oauthemail)) { 512 // Trigger login failed event. 513 $failurereason = AUTH_LOGIN_UNAUTHORISED; 514 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 515 'reason' => $failurereason]]); 516 $event->trigger(); 517 518 $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2')); 519 $SESSION->loginerrormsg = $errormsg; 520 $client->log_out(); 521 redirect(new moodle_url('/login/index.php')); 522 } 523 524 if (!$userwasmapped) { 525 // No defined mapping - we need to see if there is an existing account with the same email. 526 527 $moodleuser = \core_user::get_user_by_email($userinfo['email']); 528 if (!empty($moodleuser)) { 529 if ($issuer->get('requireconfirmation')) { 530 $PAGE->set_url('/auth/oauth2/confirm-link-login.php'); 531 $PAGE->set_context(context_system::instance()); 532 533 \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id); 534 // Request to link to existing account. 535 $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2'); 536 $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email); 537 $this->print_confirm_required($emailconfirm, $message); 538 exit(); 539 } else { 540 \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true); 541 // We dont have profile loaded on $moodleuser, so load it. 542 require_once($CFG->dirroot.'/user/profile/lib.php'); 543 profile_load_custom_fields($moodleuser); 544 $userinfo = $this->update_user($userinfo, $moodleuser); 545 // No redirect, we will complete this login. 546 } 547 548 } else { 549 // This is a new account. 550 $exists = \core_user::get_user_by_username($userinfo['username']); 551 // Creating a new user? 552 if ($exists) { 553 // Trigger login failed event. 554 $failurereason = AUTH_LOGIN_FAILED; 555 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 556 'reason' => $failurereason]]); 557 $event->trigger(); 558 559 // The username exists but the emails don't match. Refuse to continue. 560 $errormsg = get_string('accountexists', 'auth_oauth2'); 561 $SESSION->loginerrormsg = $errormsg; 562 $client->log_out(); 563 redirect(new moodle_url('/login/index.php')); 564 } 565 566 if (email_is_not_allowed($userinfo['email'])) { 567 // Trigger login failed event. 568 $failurereason = AUTH_LOGIN_FAILED; 569 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 570 'reason' => $failurereason]]); 571 $event->trigger(); 572 // The username exists but the emails don't match. Refuse to continue. 573 $reason = get_string('loginerror_invaliddomain', 'auth_oauth2'); 574 $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); 575 $SESSION->loginerrormsg = $errormsg; 576 $client->log_out(); 577 redirect(new moodle_url('/login/index.php')); 578 } 579 580 if (!empty($CFG->authpreventaccountcreation)) { 581 // Trigger login failed event. 582 $failurereason = AUTH_LOGIN_UNAUTHORISED; 583 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 584 'reason' => $failurereason]]); 585 $event->trigger(); 586 // The username does not exist and settings prevent creating new accounts. 587 $reason = get_string('loginerror_cannotcreateaccounts', 'auth_oauth2'); 588 $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); 589 $SESSION->loginerrormsg = $errormsg; 590 $client->log_out(); 591 redirect(new moodle_url('/login/index.php')); 592 } 593 594 if ($issuer->get('requireconfirmation')) { 595 $PAGE->set_url('/auth/oauth2/confirm-account.php'); 596 $PAGE->set_context(context_system::instance()); 597 598 // Create a new (unconfirmed account) and send an email to confirm it. 599 $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer); 600 601 $this->update_picture($user); 602 $emailconfirm = get_string('emailconfirm'); 603 $message = get_string('emailconfirmsent', '', $userinfo['email']); 604 $this->print_confirm_required($emailconfirm, $message); 605 exit(); 606 } else { 607 // Create a new confirmed account. 608 $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer); 609 $userinfo = get_complete_user_data('id', $newuser->id); 610 // No redirect, we will complete this login. 611 } 612 } 613 } 614 615 // We used to call authenticate_user - but that won't work if the current user has a different default authentication 616 // method. Since we now ALWAYS link a login - if we get to here we can directly allow the user in. 617 $user = (object) $userinfo; 618 619 // Add extra loggedin info. 620 $this->set_extrauserinfo((array)$rawuserinfo); 621 622 complete_user_login($user, $this->get_extrauserinfo()); 623 $this->update_picture($user); 624 redirect($redirecturl); 625 } 626 627 /** 628 * Returns information on how the specified user can change their password. 629 * The password of the oauth2 accounts is not stored in Moodle. 630 * 631 * @param stdClass $user A user object 632 * @return string[] An array of strings with keys subject and message 633 */ 634 public function get_password_change_info(stdClass $user) : array { 635 $site = get_site(); 636 637 $data = new stdClass(); 638 $data->firstname = $user->firstname; 639 $data->lastname = $user->lastname; 640 $data->username = $user->username; 641 $data->sitename = format_string($site->fullname); 642 $data->admin = generate_email_signoff(); 643 644 $message = get_string('emailpasswordchangeinfo', 'auth_oauth2', $data); 645 $subject = get_string('emailpasswordchangeinfosubject', 'auth_oauth2', format_string($site->fullname)); 646 647 return [ 648 'subject' => $subject, 649 'message' => $message 650 ]; 651 } 652 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body