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