Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]

   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  }