Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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   * Class for loading/storing oauth2 linked logins from the DB.
  19   *
  20   * @package    auth_oauth2
  21   * @copyright  2017 Damyon Wiese
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace auth_oauth2;
  25  
  26  use context_user;
  27  use stdClass;
  28  use moodle_exception;
  29  use moodle_url;
  30  
  31  defined('MOODLE_INTERNAL') || die();
  32  
  33  /**
  34   * Static list of api methods for auth oauth2 configuration.
  35   *
  36   * @package    auth_oauth2
  37   * @copyright  2017 Damyon Wiese
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class api {
  41  
  42      /**
  43       * Remove all linked logins that are using issuers that have been deleted.
  44       *
  45       * @param int $issuerid The issuer id of the issuer to check, or false to check all (defaults to all)
  46       * @return boolean
  47       */
  48      public static function clean_orphaned_linked_logins($issuerid = false) {
  49          return linked_login::delete_orphaned($issuerid);
  50      }
  51  
  52      /**
  53       * List linked logins
  54       *
  55       * Requires auth/oauth2:managelinkedlogins capability at the user context.
  56       *
  57       * @param int $userid (defaults to $USER->id)
  58       * @return boolean
  59       */
  60      public static function get_linked_logins($userid = false) {
  61          global $USER;
  62  
  63          if ($userid === false) {
  64              $userid = $USER->id;
  65          }
  66  
  67          if (\core\session\manager::is_loggedinas()) {
  68              throw new moodle_exception('notwhileloggedinas', 'auth_oauth2');
  69          }
  70  
  71          $context = context_user::instance($userid);
  72          require_capability('auth/oauth2:managelinkedlogins', $context);
  73  
  74          return linked_login::get_records(['userid' => $userid, 'confirmtoken' => '']);
  75      }
  76  
  77      /**
  78       * See if there is a match for this username and issuer in the linked_login table.
  79       *
  80       * @param string $username as returned from an oauth client.
  81       * @param \core\oauth2\issuer $issuer
  82       * @return stdClass User record if found.
  83       */
  84      public static function match_username_to_user($username, $issuer) {
  85          $params = [
  86              'issuerid' => $issuer->get('id'),
  87              'username' => $username
  88          ];
  89          $result = linked_login::get_record($params);
  90  
  91          if ($result) {
  92              $user = \core_user::get_user($result->get('userid'));
  93              if (!empty($user) && !$user->deleted) {
  94                  return $result;
  95              }
  96          }
  97          return false;
  98      }
  99  
 100      /**
 101       * Link a login to this account.
 102       *
 103       * Requires auth/oauth2:managelinkedlogins capability at the user context.
 104       *
 105       * @param array $userinfo as returned from an oauth client.
 106       * @param \core\oauth2\issuer $issuer
 107       * @param int $userid (defaults to $USER->id)
 108       * @param bool $skippermissions During signup we need to set this before the user is setup for capability checks.
 109       * @return bool
 110       */
 111      public static function link_login($userinfo, $issuer, $userid = false, $skippermissions = false) {
 112          global $USER;
 113  
 114          if ($userid === false) {
 115              $userid = $USER->id;
 116          }
 117  
 118          if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) {
 119              throw new moodle_exception('alreadylinked', 'auth_oauth2');
 120          }
 121  
 122          if (\core\session\manager::is_loggedinas()) {
 123              throw new moodle_exception('notwhileloggedinas', 'auth_oauth2');
 124          }
 125  
 126          $context = context_user::instance($userid);
 127          if (!$skippermissions) {
 128              require_capability('auth/oauth2:managelinkedlogins', $context);
 129          }
 130  
 131          $record = new stdClass();
 132          $record->issuerid = $issuer->get('id');
 133          $record->username = $userinfo['username'];
 134          $record->userid = $userid;
 135          $existing = linked_login::get_record((array)$record);
 136          if ($existing) {
 137              $existing->set('confirmtoken', '');
 138              $existing->update();
 139              return $existing;
 140          }
 141          $record->email = $userinfo['email'];
 142          $record->confirmtoken = '';
 143          $record->confirmtokenexpires = 0;
 144          $linkedlogin = new linked_login(0, $record);
 145          return $linkedlogin->create();
 146      }
 147  
 148      /**
 149       * Send an email with a link to confirm linking this account.
 150       *
 151       * @param array $userinfo as returned from an oauth client.
 152       * @param \core\oauth2\issuer $issuer
 153       * @param int $userid (defaults to $USER->id)
 154       * @return bool
 155       */
 156      public static function send_confirm_link_login_email($userinfo, $issuer, $userid) {
 157          $record = new stdClass();
 158          $record->issuerid = $issuer->get('id');
 159          $record->username = $userinfo['username'];
 160          $record->userid = $userid;
 161          if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) {
 162              throw new moodle_exception('alreadylinked', 'auth_oauth2');
 163          }
 164          $record->email = $userinfo['email'];
 165          $record->confirmtoken = random_string(32);
 166          $expires = new \DateTime('NOW');
 167          $expires->add(new \DateInterval('PT30M'));
 168          $record->confirmtokenexpires = $expires->getTimestamp();
 169  
 170          $linkedlogin = new linked_login(0, $record);
 171          $linkedlogin->create();
 172  
 173          // Construct the email.
 174          $site = get_site();
 175          $supportuser = \core_user::get_support_user();
 176          $user = get_complete_user_data('id', $userid);
 177  
 178          $data = new stdClass();
 179          $data->fullname = fullname($user);
 180          $data->sitename  = format_string($site->fullname);
 181          $data->admin     = generate_email_signoff();
 182          $data->issuername = format_string($issuer->get('name'));
 183          $data->linkedemail = format_string($linkedlogin->get('email'));
 184  
 185          $subject = get_string('confirmlinkedloginemailsubject', 'auth_oauth2', format_string($site->fullname));
 186  
 187          $params = [
 188              'token' => $linkedlogin->get('confirmtoken'),
 189              'userid' => $userid,
 190              'username' => $userinfo['username'],
 191              'issuerid' => $issuer->get('id'),
 192          ];
 193          $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params);
 194  
 195          $data->link = $confirmationurl->out(false);
 196          $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
 197  
 198          $data->link = $confirmationurl->out();
 199          $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
 200  
 201          $user->mailformat = 1;  // Always send HTML version as well.
 202  
 203          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
 204          return email_to_user($user, $supportuser, $subject, $message, $messagehtml);
 205      }
 206  
 207      /**
 208       * Look for a waiting confirmation token, and if we find a match - confirm it.
 209       *
 210       * @param int $userid
 211       * @param string $username
 212       * @param int $issuerid
 213       * @param string $token
 214       * @return boolean True if we linked.
 215       */
 216      public static function confirm_link_login($userid, $username, $issuerid, $token) {
 217          if (empty($token) || empty($userid) || empty($issuerid) || empty($username)) {
 218              return false;
 219          }
 220          $params = [
 221              'userid' => $userid,
 222              'username' => $username,
 223              'issuerid' => $issuerid,
 224              'confirmtoken' => $token,
 225          ];
 226  
 227          $login = linked_login::get_record($params);
 228          if (empty($login)) {
 229              return false;
 230          }
 231          $expires = $login->get('confirmtokenexpires');
 232          if (time() > $expires) {
 233              $login->delete();
 234              return;
 235          }
 236          $login->set('confirmtokenexpires', 0);
 237          $login->set('confirmtoken', '');
 238          $login->update();
 239          return true;
 240      }
 241  
 242      /**
 243       * Create an account with a linked login that is already confirmed.
 244       *
 245       * @param array $userinfo as returned from an oauth client.
 246       * @param \core\oauth2\issuer $issuer
 247       * @return bool
 248       */
 249      public static function create_new_confirmed_account($userinfo, $issuer) {
 250          global $CFG, $DB;
 251          require_once($CFG->dirroot.'/user/profile/lib.php');
 252          require_once($CFG->dirroot.'/user/lib.php');
 253  
 254          $user = new stdClass();
 255          $user->auth = 'oauth2';
 256          $user->mnethostid = $CFG->mnet_localhost_id;
 257          $user->secret = random_string(15);
 258          $user->password = '';
 259          $user->confirmed = 1;  // Set the user to confirmed.
 260  
 261          $user = self::save_user($userinfo, $user);
 262  
 263          // The linked account is pre-confirmed.
 264          $record = new stdClass();
 265          $record->issuerid = $issuer->get('id');
 266          $record->username = $userinfo['username'];
 267          $record->userid = $user->id;
 268          $record->email = $userinfo['email'];
 269          $record->confirmtoken = '';
 270          $record->confirmtokenexpires = 0;
 271  
 272          $linkedlogin = new linked_login(0, $record);
 273          $linkedlogin->create();
 274  
 275          return $user;
 276      }
 277  
 278      /**
 279       * Send an email with a link to confirm creating this account.
 280       *
 281       * @param array $userinfo as returned from an oauth client.
 282       * @param \core\oauth2\issuer $issuer
 283       * @param int $userid (defaults to $USER->id)
 284       * @return bool
 285       */
 286      public static function send_confirm_account_email($userinfo, $issuer) {
 287          global $CFG, $DB;
 288          require_once($CFG->dirroot.'/user/profile/lib.php');
 289          require_once($CFG->dirroot.'/user/lib.php');
 290  
 291          if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) {
 292              throw new moodle_exception('alreadylinked', 'auth_oauth2');
 293          }
 294  
 295          $user = new stdClass();
 296          $user->auth = 'oauth2';
 297          $user->mnethostid = $CFG->mnet_localhost_id;
 298          $user->secret = random_string(15);
 299          $user->password = '';
 300          $user->confirmed = 0;  // The user is not yet confirmed.
 301  
 302          $user = self::save_user($userinfo, $user);
 303  
 304          // The linked account is pre-confirmed.
 305          $record = new stdClass();
 306          $record->issuerid = $issuer->get('id');
 307          $record->username = $userinfo['username'];
 308          $record->userid = $user->id;
 309          $record->email = $userinfo['email'];
 310          $record->confirmtoken = '';
 311          $record->confirmtokenexpires = 0;
 312  
 313          $linkedlogin = new linked_login(0, $record);
 314          $linkedlogin->create();
 315  
 316          // Construct the email.
 317          $site = get_site();
 318          $supportuser = \core_user::get_support_user();
 319          $user = get_complete_user_data('id', $user->id);
 320  
 321          $data = new stdClass();
 322          $data->fullname = fullname($user);
 323          $data->sitename  = format_string($site->fullname);
 324          $data->admin     = generate_email_signoff();
 325  
 326          $subject = get_string('confirmaccountemailsubject', 'auth_oauth2', format_string($site->fullname));
 327  
 328          $params = [
 329              'token' => $user->secret,
 330              'username' => $userinfo['username']
 331          ];
 332          $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params);
 333  
 334          $data->link = $confirmationurl->out(false);
 335          $message = get_string('confirmaccountemail', 'auth_oauth2', $data);
 336  
 337          $data->link = $confirmationurl->out();
 338          $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
 339  
 340          $user->mailformat = 1;  // Always send HTML version as well.
 341  
 342          // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber.
 343          email_to_user($user, $supportuser, $subject, $message, $messagehtml);
 344          return $user;
 345      }
 346  
 347      /**
 348       * Delete linked login
 349       *
 350       * Requires auth/oauth2:managelinkedlogins capability at the user context.
 351       *
 352       * @param int $linkedloginid
 353       * @return boolean
 354       */
 355      public static function delete_linked_login($linkedloginid) {
 356          $login = new linked_login($linkedloginid);
 357          $userid = $login->get('userid');
 358  
 359          if (\core\session\manager::is_loggedinas()) {
 360              throw new moodle_exception('notwhileloggedinas', 'auth_oauth2');
 361          }
 362  
 363          $context = context_user::instance($userid);
 364          require_capability('auth/oauth2:managelinkedlogins', $context);
 365  
 366          $login->delete();
 367      }
 368  
 369      /**
 370       * Delete linked logins for a user.
 371       *
 372       * @param \core\event\user_deleted $event
 373       * @return boolean
 374       */
 375      public static function user_deleted(\core\event\user_deleted $event) {
 376          global $DB;
 377  
 378          $userid = $event->objectid;
 379  
 380          return $DB->delete_records(linked_login::TABLE, ['userid' => $userid]);
 381      }
 382  
 383      /**
 384       * Is the plugin enabled.
 385       *
 386       * @return bool
 387       */
 388      public static function is_enabled() {
 389          return is_enabled_auth('oauth2');
 390      }
 391  
 392      /**
 393       * Create a new user & update the profile fields
 394       *
 395       * @param array $userinfo
 396       * @param object $user
 397       * @return object
 398       */
 399      private static function save_user(array $userinfo, object $user): object {
 400          // Map supplied issuer user info to Moodle user fields.
 401          $userfieldmapping = new \core\oauth2\user_field_mapping();
 402          $userfieldlist = $userfieldmapping->get_internalfields();
 403          $hasprofilefield = false;
 404          foreach ($userfieldlist as $field) {
 405              if (isset($userinfo[$field]) && $userinfo[$field]) {
 406                  $user->$field = $userinfo[$field];
 407  
 408                  // Check whether the profile fields exist or not.
 409                  $hasprofilefield = $hasprofilefield || strpos($field, \core_user\fields::PROFILE_FIELD_PREFIX) === 0;
 410              }
 411          }
 412  
 413          // Create a new user.
 414          $user->id = user_create_user($user, false, true);
 415  
 416          // If profile fields exist then save custom profile fields data.
 417          if ($hasprofilefield) {
 418              profile_save_data($user);
 419          }
 420  
 421          return $user;
 422      }
 423  }