Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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   * 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              print_error( 'shib_not_all_attributes_error', 'auth_shibboleth' , '', "'".$this->config->user_attribute."' ('".$_SERVER[$this->config->user_attribute]."'), '".$this->config->field_map_firstname."' ('".$_SERVER[$this->config->field_map_firstname]."'), '".$this->config->field_map_lastname."' ('".$_SERVER[$this->config->field_map_lastname]."') and '".$this->config->field_map_email."' ('".$_SERVER[$this->config->field_map_email]."')");
 109          }
 110  
 111          $attrmap = $this->get_attributes();
 112  
 113          $result = array();
 114          $search_attribs = array();
 115  
 116          foreach ($attrmap as $key=>$value) {
 117              // Check if attribute is present
 118              if (!isset($_SERVER[$value])){
 119                  $result[$key] = '';
 120                  continue;
 121              }
 122  
 123              // Make usename lowercase
 124              if ($key == 'username'){
 125                  $result[$key] = strtolower($this->get_first_string($_SERVER[$value]));
 126              } else {
 127                  $result[$key] = $this->get_first_string($_SERVER[$value]);
 128              }
 129          }
 130  
 131           // Provide an API to modify the information to fit the Moodle internal
 132          // data representation
 133          if (
 134                $this->config->convert_data
 135                && $this->config->convert_data != ''
 136                && is_readable($this->config->convert_data)
 137              ) {
 138  
 139              // Include a custom file outside the Moodle dir to
 140              // modify the variable $moodleattributes
 141              include($this->config->convert_data);
 142          }
 143  
 144          return $result;
 145      }
 146  
 147      /**
 148       * Returns array containg attribute mappings between Moodle and Shibboleth.
 149       *
 150       * @return array
 151       */
 152      function get_attributes() {
 153          $configarray = (array) $this->config;
 154  
 155          $moodleattributes = array();
 156          $userfields = array_merge($this->userfields, $this->get_custom_user_profile_fields());
 157          foreach ($userfields as $field) {
 158              if (isset($configarray["field_map_$field"])) {
 159                  $moodleattributes[$field] = $configarray["field_map_$field"];
 160              }
 161          }
 162          $moodleattributes['username'] = $configarray["user_attribute"];
 163  
 164          return $moodleattributes;
 165      }
 166  
 167      function prevent_local_passwords() {
 168          return true;
 169      }
 170  
 171      /**
 172       * Returns true if this authentication plugin is 'internal'.
 173       *
 174       * @return bool
 175       */
 176      function is_internal() {
 177          return false;
 178      }
 179  
 180      /**
 181       * Whether shibboleth users can change their password or not.
 182       *
 183       * Shibboleth auth requires password to be changed on shibboleth server directly.
 184       * So it is required to have  password change url set.
 185       *
 186       * @return bool true if there's a password url or false otherwise.
 187       */
 188      function can_change_password() {
 189          if (!empty($this->config->changepasswordurl)) {
 190              return true;
 191          } else {
 192              return false;
 193          }
 194      }
 195  
 196      /**
 197       * Get password change url.
 198       *
 199       * @return moodle_url|null Returns URL to change password or null otherwise.
 200       */
 201      function change_password_url() {
 202          if (!empty($this->config->changepasswordurl)) {
 203              return new moodle_url($this->config->changepasswordurl);
 204          } else {
 205              return null;
 206          }
 207      }
 208  
 209       /**
 210       * Hook for login page
 211       *
 212       */
 213      function loginpage_hook() {
 214          global $SESSION, $CFG;
 215  
 216          // Prevent username from being shown on login page after logout
 217          $CFG->nolastloggedin = true;
 218  
 219          return;
 220      }
 221  
 222       /**
 223       * Hook for logout page
 224       *
 225       */
 226      function logoutpage_hook() {
 227          global $SESSION, $redirect;
 228  
 229          // Only do this if logout handler is defined, and if the user is actually logged in via Shibboleth
 230          $logouthandlervalid = isset($this->config->logout_handler) && !empty($this->config->logout_handler);
 231          if (isset($SESSION->shibboleth_session_id) && $logouthandlervalid ) {
 232              // Check if there is an alternative logout return url defined
 233              if (isset($this->config->logout_return_url) && !empty($this->config->logout_return_url)) {
 234                  // Set temp_redirect to alternative return url
 235                  $temp_redirect = $this->config->logout_return_url;
 236              } else {
 237                  // Backup old redirect url
 238                  $temp_redirect = $redirect;
 239              }
 240  
 241              // Overwrite redirect in order to send user to Shibboleth logout page and let him return back
 242              $redirecturl = new moodle_url($this->config->logout_handler, array('return' => $temp_redirect));
 243              $redirect = $redirecturl->out();
 244          }
 245      }
 246  
 247      /**
 248       * Cleans and returns first of potential many values (multi-valued attributes)
 249       *
 250       * @param string $string Possibly multi-valued attribute from Shibboleth
 251       */
 252      function get_first_string($string) {
 253          $list = explode( ';', $string);
 254          $clean_string = rtrim($list[0]);
 255  
 256          return $clean_string;
 257      }
 258  
 259      /**
 260       * Test if settings are correct, print info to output.
 261       */
 262      public function test_settings() {
 263          global $OUTPUT;
 264  
 265          if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) {
 266              echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth",
 267                  (new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem');
 268              return;
 269          }
 270          if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) {
 271              echo $OUTPUT->notification(get_string("auth_shib_convert_data_warning", "auth_shibboleth"), 'notifyproblem');
 272              return;
 273          }
 274          if (isset($this->config->organization_selection) && empty($this->config->organization_selection) &&
 275                  isset($this->config->alt_login) && $this->config->alt_login == 'on') {
 276  
 277              echo $OUTPUT->notification(get_string("auth_shib_no_organizations_warning", "auth_shibboleth"), 'notifyproblem');
 278              return;
 279          }
 280      }
 281  
 282      /**
 283       * Return a list of identity providers to display on the login page.
 284       *
 285       * @param string $wantsurl The requested URL.
 286       * @return array List of arrays with keys url, iconurl and name.
 287       */
 288      public function loginpage_idp_list($wantsurl) {
 289          $config = get_config('auth_shibboleth');
 290          $result = [];
 291  
 292          // Before displaying the button check that Shibboleth is set-up correctly.
 293          if (empty($config->user_attribute)) {
 294              return $result;
 295          }
 296  
 297          $url = new moodle_url('/auth/shibboleth/index.php');
 298  
 299          if ($config->auth_logo) {
 300              $iconurl = moodle_url::make_pluginfile_url(
 301                  context_system::instance()->id,
 302                  'auth_shibboleth',
 303                  'logo',
 304                  null,
 305                  null,
 306                  $config->auth_logo);
 307          } else {
 308              $iconurl = null;
 309          }
 310  
 311          $result[] = ['url' => $url, 'iconurl' => $iconurl, 'name' => $config->login_name];
 312          return $result;
 313      }
 314  }
 315  
 316  
 317      /**
 318       * Sets the standard SAML domain cookie that is also used to preselect
 319       * the right entry on the local way
 320       *
 321       * @param string $selectedIDP IDP identifier
 322       */
 323      function set_saml_cookie($selectedIDP) {
 324          if (isset($_COOKIE['_saml_idp']))
 325          {
 326              $IDPArray = generate_cookie_array($_COOKIE['_saml_idp']);
 327          }
 328          else
 329          {
 330              $IDPArray = array();
 331          }
 332          $IDPArray = appendCookieValue($selectedIDP, $IDPArray);
 333          setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600));
 334      }
 335  
 336      /**
 337       * Generate array of IdPs from Moodle Shibboleth settings
 338       *
 339       * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator
 340       * @return array Identifier of IdPs and their name/session initiator
 341       */
 342      function get_idp_list($organization_selection) {
 343          $idp_list = array();
 344  
 345          $idp_raw_list = explode("\n",  $organization_selection);
 346  
 347          foreach ($idp_raw_list as $idp_line){
 348              $idp_data = explode(',', $idp_line);
 349              if (isset($idp_data[2]))
 350              {
 351                  $idp_list[trim($idp_data[0])] = array(trim($idp_data[1]),trim($idp_data[2]));
 352              }
 353              elseif(isset($idp_data[1]))
 354              {
 355                  $idp_list[trim($idp_data[0])] = array(trim($idp_data[1]));
 356              }
 357          }
 358  
 359          return $idp_list;
 360      }
 361  
 362      /**
 363       * Generates an array of IDPs using the cookie value
 364       *
 365       * @param string Value of SAML domain cookie
 366       * @return array Identifiers of IdPs
 367       */
 368      function generate_cookie_array($value) {
 369  
 370          // Decodes and splits cookie value
 371          $CookieArray = explode(' ', $value);
 372          $CookieArray = array_map('base64_decode', $CookieArray);
 373  
 374          return $CookieArray;
 375      }
 376  
 377      /**
 378       * Generate the value that is stored in the cookie using the list of IDPs
 379       *
 380       * @param array IdP identifiers
 381       * @return string SAML domain cookie value
 382       */
 383      function generate_cookie_value($CookieArray) {
 384  
 385          // Merges cookie content and encodes it
 386          $CookieArray = array_map('base64_encode', $CookieArray);
 387          $value = implode(' ', $CookieArray);
 388          return $value;
 389      }
 390  
 391      /**
 392       * Append a value to the array of IDPs
 393       *
 394       * @param string IdP identifier
 395       * @param array IdP identifiers
 396       * @return array IdP identifiers with appended IdP
 397       */
 398      function appendCookieValue($value, $CookieArray) {
 399  
 400          array_push($CookieArray, $value);
 401          $CookieArray = array_reverse($CookieArray);
 402          $CookieArray = array_unique($CookieArray);
 403          $CookieArray = array_reverse($CookieArray);
 404  
 405          return $CookieArray;
 406      }
 407  
 408  
 409