See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [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 * Configurable oauth2 client class. 19 * 20 * @package core 21 * @copyright 2017 Damyon Wiese 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core\oauth2; 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->libdir . '/oauthlib.php'); 29 require_once($CFG->libdir . '/filelib.php'); 30 31 use moodle_url; 32 use moodle_exception; 33 use stdClass; 34 35 /** 36 * Configurable oauth2 client class. URLs come from DB and access tokens from either DB (system accounts) or session (users'). 37 * 38 * @copyright 2017 Damyon Wiese 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 */ 41 class client extends \oauth2_client { 42 43 /** @var \core\oauth2\issuer $issuer */ 44 private $issuer; 45 46 /** @var bool $system */ 47 protected $system = false; 48 49 /** 50 * Constructor. 51 * 52 * @param issuer $issuer 53 * @param moodle_url|null $returnurl 54 * @param string $scopesrequired 55 * @param boolean $system 56 */ 57 public function __construct(issuer $issuer, $returnurl, $scopesrequired, $system = false) { 58 $this->issuer = $issuer; 59 $this->system = $system; 60 $scopes = $this->get_login_scopes(); 61 $additionalscopes = explode(' ', $scopesrequired); 62 63 foreach ($additionalscopes as $scope) { 64 if (!empty($scope)) { 65 if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { 66 $scopes .= ' ' . $scope; 67 } 68 } 69 } 70 if (empty($returnurl)) { 71 $returnurl = new moodle_url('/'); 72 } 73 $this->basicauth = $issuer->get('basicauth'); 74 parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes); 75 } 76 77 /** 78 * Returns the auth url for OAuth 2.0 request 79 * @return string the auth url 80 */ 81 protected function auth_url() { 82 return $this->issuer->get_endpoint_url('authorization'); 83 } 84 85 /** 86 * Get the oauth2 issuer for this client. 87 * 88 * @return \core\oauth2\issuer Issuer 89 */ 90 public function get_issuer() { 91 return $this->issuer; 92 } 93 94 /** 95 * Override to append additional params to a authentication request. 96 * 97 * @return array (name value pairs). 98 */ 99 public function get_additional_login_parameters() { 100 $params = ''; 101 if ($this->system) { 102 if (!empty($this->issuer->get('loginparamsoffline'))) { 103 $params = $this->issuer->get('loginparamsoffline'); 104 } 105 } else { 106 if (!empty($this->issuer->get('loginparams'))) { 107 $params = $this->issuer->get('loginparams'); 108 } 109 } 110 if (empty($params)) { 111 return []; 112 } 113 $result = []; 114 parse_str($params, $result); 115 return $result; 116 } 117 118 /** 119 * Override to change the scopes requested with an authentiction request. 120 * 121 * @return string 122 */ 123 protected function get_login_scopes() { 124 if ($this->system) { 125 return $this->issuer->get('loginscopesoffline'); 126 } else { 127 return $this->issuer->get('loginscopes'); 128 } 129 } 130 131 /** 132 * Returns the token url for OAuth 2.0 request 133 * 134 * We are overriding the parent function so we get this from the configured endpoint. 135 * 136 * @return string the auth url 137 */ 138 protected function token_url() { 139 return $this->issuer->get_endpoint_url('token'); 140 } 141 142 /** 143 * We want a unique key for each issuer / and a different key for system vs user oauth. 144 * 145 * @return string The unique key for the session value. 146 */ 147 protected function get_tokenname() { 148 $name = 'oauth2-state-' . $this->issuer->get('id'); 149 if ($this->system) { 150 $name .= '-system'; 151 } 152 return $name; 153 } 154 155 /** 156 * Store a token between requests. Uses session named by get_tokenname for user account tokens 157 * and a database record for system account tokens. 158 * 159 * @param stdClass|null $token token object to store or null to clear 160 */ 161 protected function store_token($token) { 162 if (!$this->system) { 163 parent::store_token($token); 164 return; 165 } 166 167 $this->accesstoken = $token; 168 169 // Create or update a DB record with the new token. 170 $persistedtoken = access_token::get_record(['issuerid' => $this->issuer->get('id')]); 171 if ($token !== null) { 172 if (!$persistedtoken) { 173 $persistedtoken = new access_token(); 174 $persistedtoken->set('issuerid', $this->issuer->get('id')); 175 } 176 // Update values from $token. Don't use from_record because that would skip validation. 177 $persistedtoken->set('token', $token->token); 178 if (isset($token->expires)) { 179 $persistedtoken->set('expires', $token->expires); 180 } else { 181 // Assume an arbitrary time span of 1 week for access tokens without expiration. 182 // The "refresh_system_tokens_task" is run hourly (by default), so the token probably won't last that long. 183 $persistedtoken->set('expires', time() + WEEKSECS); 184 } 185 $persistedtoken->set('scope', $token->scope); 186 $persistedtoken->save(); 187 } else { 188 if ($persistedtoken) { 189 $persistedtoken->delete(); 190 } 191 } 192 } 193 194 /** 195 * Retrieve a stored token from session (user accounts) or database (system accounts). 196 * 197 * @return stdClass|null token object 198 */ 199 protected function get_stored_token() { 200 if ($this->system) { 201 $token = access_token::get_record(['issuerid' => $this->issuer->get('id')]); 202 if ($token !== false) { 203 return $token->to_record(); 204 } 205 return null; 206 } 207 208 return parent::get_stored_token(); 209 } 210 211 /** 212 * Get a list of the mapping user fields in an associative array. 213 * 214 * @return array 215 */ 216 protected function get_userinfo_mapping() { 217 $fields = user_field_mapping::get_records(['issuerid' => $this->issuer->get('id')]); 218 219 $map = []; 220 foreach ($fields as $field) { 221 $map[$field->get('externalfield')] = $field->get('internalfield'); 222 } 223 return $map; 224 } 225 226 /** 227 * Upgrade a refresh token from oauth 2.0 to an access token 228 * 229 * @param \core\oauth2\system_account $systemaccount 230 * @return boolean true if token is upgraded succesfully 231 * @throws moodle_exception Request for token upgrade failed for technical reasons 232 */ 233 public function upgrade_refresh_token(system_account $systemaccount) { 234 $refreshtoken = $systemaccount->get('refreshtoken'); 235 236 $params = array('refresh_token' => $refreshtoken, 237 'grant_type' => 'refresh_token' 238 ); 239 240 if ($this->basicauth) { 241 $idsecret = urlencode($this->issuer->get('clientid')) . ':' . urlencode($this->issuer->get('clientsecret')); 242 $this->setHeader('Authorization: Basic ' . base64_encode($idsecret)); 243 } else { 244 $params['client_id'] = $this->issuer->get('clientid'); 245 $params['client_secret'] = $this->issuer->get('clientsecret'); 246 } 247 248 // Requests can either use http GET or POST. 249 if ($this->use_http_get()) { 250 $response = $this->get($this->token_url(), $params); 251 } else { 252 $response = $this->post($this->token_url(), $this->build_post_data($params)); 253 } 254 255 if ($this->info['http_code'] !== 200) { 256 throw new moodle_exception('Could not upgrade oauth token'); 257 } 258 259 $r = json_decode($response); 260 261 if (!empty($r->error)) { 262 throw new moodle_exception($r->error . ' ' . $r->error_description); 263 } 264 265 if (!isset($r->access_token)) { 266 return false; 267 } 268 269 // Store the token an expiry time. 270 $accesstoken = new stdClass; 271 $accesstoken->token = $r->access_token; 272 if (isset($r->expires_in)) { 273 // Expires 10 seconds before actual expiry. 274 $accesstoken->expires = (time() + ($r->expires_in - 10)); 275 } 276 $accesstoken->scope = $this->scope; 277 // Also add the scopes. 278 $this->store_token($accesstoken); 279 280 if (isset($r->refresh_token)) { 281 $systemaccount->set('refreshtoken', $r->refresh_token); 282 $systemaccount->update(); 283 $this->refreshtoken = $r->refresh_token; 284 } 285 286 return true; 287 } 288 289 /** 290 * Fetch the user info from the user info endpoint and map all 291 * the fields back into moodle fields. 292 * 293 * @return array|false Moodle user fields for the logged in user (or false if request failed) 294 */ 295 public function get_userinfo() { 296 $url = $this->get_issuer()->get_endpoint_url('userinfo'); 297 $response = $this->get($url); 298 if (!$response) { 299 return false; 300 } 301 $userinfo = new stdClass(); 302 try { 303 $userinfo = json_decode($response); 304 } catch (\Exception $e) { 305 return false; 306 } 307 308 $map = $this->get_userinfo_mapping(); 309 310 $user = new stdClass(); 311 foreach ($map as $openidproperty => $moodleproperty) { 312 // We support nested objects via a-b-c syntax. 313 $getfunc = function($obj, $prop) use (&$getfunc) { 314 $proplist = explode('-', $prop, 2); 315 if (empty($proplist[0]) || empty($obj->{$proplist[0]})) { 316 return false; 317 } 318 $obj = $obj->{$proplist[0]}; 319 320 if (count($proplist) > 1) { 321 return $getfunc($obj, $proplist[1]); 322 } 323 return $obj; 324 }; 325 326 $resolved = $getfunc($userinfo, $openidproperty); 327 if (!empty($resolved)) { 328 $user->$moodleproperty = $resolved; 329 } 330 } 331 332 if (empty($user->username) && !empty($user->email)) { 333 $user->username = $user->email; 334 } 335 336 if (!empty($user->picture)) { 337 $user->picture = download_file_content($user->picture, null, null, false, 10, 10, true, null, false); 338 } else { 339 $pictureurl = $this->issuer->get_endpoint_url('userpicture'); 340 if (!empty($pictureurl)) { 341 $user->picture = $this->get($pictureurl); 342 } 343 } 344 345 if (!empty($user->picture)) { 346 // If it doesn't look like a picture lets unset it. 347 if (function_exists('imagecreatefromstring')) { 348 $img = @imagecreatefromstring($user->picture); 349 if (empty($img)) { 350 unset($user->picture); 351 } else { 352 imagedestroy($img); 353 } 354 } 355 } 356 357 return (array)$user; 358 } 359 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body