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   * Authentication Plugin: Shibboleth Authentication
  19   * Authentication using Shibboleth.
  20   *
  21   * Distributed under GPL (c)Markus Hagman 2004-2006
  22   *
  23   * @package auth_shibboleth
  24   * @author Martin Dougiamas
  25   * @author Lukas Haemmerle
  26   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  require_once($CFG->libdir.'/authlib.php');
  32  
  33  /**
  34   * Shibboleth authentication plugin.
  35   */
  36  class auth_plugin_shibboleth extends auth_plugin_base {
  37  
  38      /**
  39       * Constructor.
  40       */
  41      public function __construct() {
  42          $this->authtype = 'shibboleth';
  43          $this->config = get_config('auth_shibboleth');
  44      }
  45  
  46      /**
  47       * Old syntax of class constructor. Deprecated in PHP7.
  48       *
  49       * @deprecated since Moodle 3.1
  50       */
  51      public function auth_plugin_shibboleth() {
  52          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  53          self::__construct();
  54      }
  55  
  56      /**
  57       * Returns true if the username and password work and false if they are
  58       * wrong or don't exist.
  59       *
  60       * @param string $username The username (with system magic quotes)
  61       * @param string $password The password (with system magic quotes)
  62       * @return bool Authentication success or failure.
  63       */
  64      function user_login($username, $password) {
  65         global $SESSION;
  66  
  67          // If we are in the shibboleth directory then we trust the server var
  68          if (!empty($_SERVER[$this->config->user_attribute])) {
  69              // Associate Shibboleth session with user for SLO preparation
  70              $sessionkey = '';
  71              if (isset($_SERVER['Shib-Session-ID'])){
  72                  // This is only available for Shibboleth 2.x SPs
  73                  $sessionkey = $_SERVER['Shib-Session-ID'];
  74              } else {
  75                  // Try to find out using the user's cookie
  76                  foreach ($_COOKIE as $name => $value){
  77                      if (preg_match('/_shibsession_/i', $name)){
  78                          $sessionkey = $value;
  79                      }
  80                  }
  81              }
  82  
  83              // Set shibboleth session ID for logout
  84              $SESSION->shibboleth_session_id  = $sessionkey;
  85  
  86              return (strtolower($_SERVER[$this->config->user_attribute]) == strtolower($username));
  87          } else {
  88              // If we are not, the user has used the manual login and the login name is
  89              // unknown, so we return false.
  90              return false;
  91          }
  92      }
  93  
  94  
  95  
  96      /**
  97       * Returns the user information for 'external' users. In this case the
  98       * attributes provided by Shibboleth
  99       *
 100       * @return array $result Associative array of user data
 101       */
 102      function get_userinfo($username) {
 103      // reads user information from shibboleth attributes and return it in array()
 104          global $CFG;
 105  
 106          // Check whether we have got all the essential attributes
 107          if ( empty($_SERVER[$this->config->user_attribute]) ) {
 108              throw new \moodle_exception( 'shib_not_all_attributes_error', 'auth_shibboleth' , '',
 109                      "'".$this->config->user_attribute."' ('".$_SERVER[$this->config->user_attribute]."'), '".
 110                  $this->config->field_map_firstname."' ('".$_SERVER[$this->config->field_map_firstname]."'), '".
 111                  $this->config->field_map_lastname."' ('".$_SERVER[$this->config->field_map_lastname]."') and '".
 112                  $this->config->field_map_email."' ('".$_SERVER[$this->config->field_map_email]."')");
 113          }
 114  
 115          $attrmap = $this->get_attributes();
 116  
 117          $result = array();
 118          $search_attribs = array();
 119  
 120          foreach ($attrmap as $key=>$value) {
 121              // Check if attribute is present
 122              if (!isset($_SERVER[$value])){
 123                  $result[$key] = '';
 124                  continue;
 125              }
 126  
 127              // Make usename lowercase
 128              if ($key == 'username'){
 129                  $result[$key] = strtolower($this->get_first_string($_SERVER[$value]));
 130              } else {
 131                  $result[$key] = $this->get_first_string($_SERVER[$value]);
 132              }
 133          }
 134  
 135           // Provide an API to modify the information to fit the Moodle internal
 136          // data representation
 137          if (
 138                $this->config->convert_data
 139                && $this->config->convert_data != ''
 140                && is_readable($this->config->convert_data)
 141              ) {
 142  
 143              // Include a custom file outside the Moodle dir to
 144              // modify the variable $moodleattributes
 145              include($this->config->convert_data);
 146          }
 147  
 148          return $result;
 149      }
 150  
 151      /**
 152       * Returns array containg attribute mappings between Moodle and Shibboleth.
 153       *
 154       * @return array
 155       */
 156      function get_attributes() {
 157          $configarray = (array) $this->config;
 158  
 159          $moodleattributes = array();
 160          $userfields = array_merge($this->userfields, $this->get_custom_user_profile_fields());
 161          foreach ($userfields as $field) {
 162              if (isset($configarray["field_map_$field"])) {
 163                  $moodleattributes[$field] = $configarray["field_map_$field"];
 164              }
 165          }
 166          $moodleattributes['username'] = $configarray["user_attribute"];
 167  
 168          return $moodleattributes;
 169      }
 170  
 171      function prevent_local_passwords() {
 172          return true;
 173      }
 174  
 175      /**
 176       * Returns true if this authentication plugin is 'internal'.
 177       *
 178       * @return bool
 179       */
 180      function is_internal() {
 181          return false;
 182      }
 183  
 184      /**
 185       * Whether shibboleth users can change their password or not.
 186       *
 187       * Shibboleth auth requires password to be changed on shibboleth server directly.
 188       * So it is required to have  password change url set.
 189       *
 190       * @return bool true if there's a password url or false otherwise.
 191       */
 192      function can_change_password() {
 193          if (!empty($this->config->changepasswordurl)) {
 194              return true;
 195          } else {
 196              return false;
 197          }
 198      }
 199  
 200      /**
 201       * Get password change url.
 202       *
 203       * @return moodle_url|null Returns URL to change password or null otherwise.
 204       */
 205      function change_password_url() {
 206          if (!empty($this->config->changepasswordurl)) {
 207              return new moodle_url($this->config->changepasswordurl);
 208          } else {
 209              return null;
 210          }
 211      }
 212  
 213       /**
 214       * Hook for login page
 215       *
 216       */
 217      function loginpage_hook() {
 218          global $SESSION, $CFG;
 219  
 220          // Prevent username from being shown on login page after logout
 221          $CFG->nolastloggedin = true;
 222  
 223          return;
 224      }
 225  
 226       /**
 227       * Hook for logout page
 228       *
 229       */
 230      function logoutpage_hook() {
 231          global $SESSION, $redirect;
 232  
 233          // Only do this if logout handler is defined, and if the user is actually logged in via Shibboleth
 234          $logouthandlervalid = isset($this->config->logout_handler) && !empty($this->config->logout_handler);
 235          if (isset($SESSION->shibboleth_session_id) && $logouthandlervalid ) {
 236              // Check if there is an alternative logout return url defined
 237              if (isset($this->config->logout_return_url) && !empty($this->config->logout_return_url)) {
 238                  // Set temp_redirect to alternative return url
 239                  $temp_redirect = $this->config->logout_return_url;
 240              } else {
 241                  // Backup old redirect url
 242                  $temp_redirect = $redirect;
 243              }
 244  
 245              // Overwrite redirect in order to send user to Shibboleth logout page and let him return back
 246              $redirecturl = new moodle_url($this->config->logout_handler, array('return' => $temp_redirect));
 247              $redirect = $redirecturl->out();
 248          }
 249      }
 250  
 251      /**
 252       * Cleans and returns first of potential many values (multi-valued attributes)
 253       *
 254       * @param string $string Possibly multi-valued attribute from Shibboleth
 255       */
 256      function get_first_string($string) {
 257          $list = explode( ';', $string);
 258          $clean_string = rtrim($list[0]);
 259  
 260          return $clean_string;
 261      }
 262  
 263      /**
 264       * Test if settings are correct, print info to output.
 265       */
 266      public function test_settings() {
 267          global $OUTPUT;
 268  
 269          if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) {
 270              echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth",
 271                  (new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem');
 272              return;
 273          }
 274          if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) {
 275              echo $OUTPUT->notification(get_string("auth_shib_convert_data_warning", "auth_shibboleth"), 'notifyproblem');
 276              return;
 277          }
 278          if (isset($this->config->organization_selection) && empty($this->config->organization_selection) &&
 279                  isset($this->config->alt_login) && $this->config->alt_login == 'on') {
 280  
 281              echo $OUTPUT->notification(get_string("auth_shib_no_organizations_warning", "auth_shibboleth"), 'notifyproblem');
 282              return;
 283          }
 284      }
 285  
 286      /**
 287       * Return a list of identity providers to display on the login page.
 288       *
 289       * @param string $wantsurl The requested URL.
 290       * @return array List of arrays with keys url, iconurl and name.
 291       */
 292      public function loginpage_idp_list($wantsurl) {
 293          $config = get_config('auth_shibboleth');
 294          $result = [];
 295  
 296          // Before displaying the button check that Shibboleth is set-up correctly.
 297          if (empty($config->user_attribute)) {
 298              return $result;
 299          }
 300  
 301          $url = new moodle_url('/auth/shibboleth/index.php');
 302  
 303          if ($config->auth_logo) {
 304              $iconurl = moodle_url::make_pluginfile_url(
 305                  context_system::instance()->id,
 306                  'auth_shibboleth',
 307                  'logo',
 308                  null,
 309                  null,
 310                  $config->auth_logo);
 311          } else {
 312              $iconurl = null;
 313          }
 314  
 315          $result[] = ['url' => $url, 'iconurl' => $iconurl, 'name' => $config->login_name];
 316          return $result;
 317      }
 318  }
 319  
 320  
 321      /**
 322       * Sets the standard SAML domain cookie that is also used to preselect
 323       * the right entry on the local way
 324       *
 325       * @param string $selectedIDP IDP identifier
 326       */
 327      function set_saml_cookie($selectedIDP) {
 328          if (isset($_COOKIE['_saml_idp']))
 329          {
 330              $IDPArray = generate_cookie_array($_COOKIE['_saml_idp']);
 331          }
 332          else
 333          {
 334              $IDPArray = array();
 335          }
 336          $IDPArray = appendCookieValue($selectedIDP, $IDPArray);
 337          setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600));
 338      }
 339  
 340      /**
 341       * Generate array of IdPs from Moodle Shibboleth settings
 342       *
 343       * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator
 344       * @return array Identifier of IdPs and their name/session initiator
 345       */
 346      function get_idp_list($organization_selection) {
 347          $idp_list = array();
 348  
 349          $idp_raw_list = explode("\n",  $organization_selection);
 350  
 351          foreach ($idp_raw_list as $idp_line){
 352              $idp_data = explode(',', $idp_line);
 353              if (isset($idp_data[2]))
 354              {
 355                  $idp_list[trim($idp_data[0])] = array(trim($idp_data[1]),trim($idp_data[2]));
 356              }
 357              elseif(isset($idp_data[1]))
 358              {
 359                  $idp_list[trim($idp_data[0])] = array(trim($idp_data[1]));
 360              }
 361          }
 362  
 363          return $idp_list;
 364      }
 365  
 366      /**
 367       * Generates an array of IDPs using the cookie value
 368       *
 369       * @param string Value of SAML domain cookie
 370       * @return array Identifiers of IdPs
 371       */
 372      function generate_cookie_array($value) {
 373  
 374          // Decodes and splits cookie value
 375          $CookieArray = explode(' ', $value);
 376          $CookieArray = array_map('base64_decode', $CookieArray);
 377  
 378          return $CookieArray;
 379      }
 380  
 381      /**
 382       * Generate the value that is stored in the cookie using the list of IDPs
 383       *
 384       * @param array IdP identifiers
 385       * @return string SAML domain cookie value
 386       */
 387      function generate_cookie_value($CookieArray) {
 388  
 389          // Merges cookie content and encodes it
 390          $CookieArray = array_map('base64_encode', $CookieArray);
 391          $value = implode(' ', $CookieArray);
 392          return $value;
 393      }
 394  
 395      /**
 396       * Append a value to the array of IDPs
 397       *
 398       * @param string IdP identifier
 399       * @param array IdP identifiers
 400       * @return array IdP identifiers with appended IdP
 401       */
 402      function appendCookieValue($value, $CookieArray) {
 403  
 404          array_push($CookieArray, $value);
 405          $CookieArray = array_reverse($CookieArray);
 406          $CookieArray = array_unique($CookieArray);
 407          $CookieArray = array_reverse($CookieArray);
 408  
 409          return $CookieArray;
 410      }
 411  
 412  
 413