See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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 defined('MOODLE_INTERNAL') || die(); 19 20 require_once($CFG->libdir.'/filelib.php'); 21 22 /** 23 * OAuth helper class 24 * 25 * 1. You can extends oauth_helper to add specific functions, such as twitter extends oauth_helper 26 * 2. Call request_token method to get oauth_token and oauth_token_secret, and redirect user to authorize_url, 27 * developer needs to store oauth_token and oauth_token_secret somewhere, we will use them to request 28 * access token later on 29 * 3. User approved the request, and get back to moodle 30 * 4. Call get_access_token, it takes previous oauth_token and oauth_token_secret as arguments, oauth_token 31 * will be used in OAuth request, oauth_token_secret will be used to bulid signature, this method will 32 * return access_token and access_secret, store these two values in database or session 33 * 5. Now you can access oauth protected resources by access_token and access_secret using oauth_helper::request 34 * method (or get() post()) 35 * 36 * Note: 37 * 1. This class only support HMAC-SHA1 38 * 2. oauth_helper class don't store tokens and secrets, you must store them manually 39 * 3. Some functions are based on http://code.google.com/p/oauth/ 40 * 41 * @package moodlecore 42 * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> 43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 */ 45 46 class oauth_helper { 47 /** @var string consumer key, issued by oauth provider*/ 48 protected $consumer_key; 49 /** @var string consumer secret, issued by oauth provider*/ 50 protected $consumer_secret; 51 /** @var string oauth root*/ 52 protected $api_root; 53 /** @var string request token url*/ 54 protected $request_token_api; 55 /** @var string authorize url*/ 56 protected $authorize_url; 57 protected $http_method; 58 /** @var string */ 59 protected $access_token_api; 60 /** @var curl */ 61 protected $http; 62 /** @var array options to pass to the next curl request */ 63 protected $http_options; 64 65 /** 66 * Contructor for oauth_helper. 67 * Subclass can override construct to build its own $this->http 68 * 69 * @param array $args requires at least three keys, oauth_consumer_key 70 * oauth_consumer_secret and api_root, oauth_helper will 71 * guess request_token_api, authrize_url and access_token_api 72 * based on api_root, but it not always works 73 */ 74 function __construct($args) { 75 if (!empty($args['api_root'])) { 76 $this->api_root = $args['api_root']; 77 } else { 78 $this->api_root = ''; 79 } 80 $this->consumer_key = $args['oauth_consumer_key']; 81 $this->consumer_secret = $args['oauth_consumer_secret']; 82 83 if (empty($args['request_token_api'])) { 84 $this->request_token_api = $this->api_root . '/request_token'; 85 } else { 86 $this->request_token_api = $args['request_token_api']; 87 } 88 89 if (empty($args['authorize_url'])) { 90 $this->authorize_url = $this->api_root . '/authorize'; 91 } else { 92 $this->authorize_url = $args['authorize_url']; 93 } 94 95 if (empty($args['access_token_api'])) { 96 $this->access_token_api = $this->api_root . '/access_token'; 97 } else { 98 $this->access_token_api = $args['access_token_api']; 99 } 100 101 if (!empty($args['oauth_callback'])) { 102 $this->oauth_callback = new moodle_url($args['oauth_callback']); 103 } 104 if (!empty($args['access_token'])) { 105 $this->access_token = $args['access_token']; 106 } 107 if (!empty($args['access_token_secret'])) { 108 $this->access_token_secret = $args['access_token_secret']; 109 } 110 $this->http = new curl(array('debug'=>false)); 111 if (!empty($args['http_options'])) { 112 $this->http_options = $args['http_options']; 113 } else { 114 $this->http_options = array(); 115 } 116 } 117 118 /** 119 * Build parameters list: 120 * oauth_consumer_key="0685bd9184jfhq22", 121 * oauth_nonce="4572616e48616d6d65724c61686176", 122 * oauth_token="ad180jjd733klru7", 123 * oauth_signature_method="HMAC-SHA1", 124 * oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 125 * oauth_timestamp="137131200", 126 * oauth_version="1.0" 127 * oauth_verifier="1.0" 128 * @param array $param 129 * @return string 130 */ 131 function get_signable_parameters($params){ 132 $sorted = $params; 133 ksort($sorted); 134 135 $total = array(); 136 foreach ($sorted as $k => $v) { 137 if ($k == 'oauth_signature') { 138 continue; 139 } 140 141 $total[] = rawurlencode($k) . '=' . rawurlencode($v); 142 } 143 return implode('&', $total); 144 } 145 146 /** 147 * Create signature for oauth request 148 * @param string $url 149 * @param string $secret 150 * @param array $params 151 * @return string 152 */ 153 public function sign($http_method, $url, $params, $secret) { 154 $sig = array( 155 strtoupper($http_method), 156 preg_replace('/%7E/', '~', rawurlencode($url)), 157 rawurlencode($this->get_signable_parameters($params)), 158 ); 159 160 $base_string = implode('&', $sig); 161 $sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true)); 162 return $sig; 163 } 164 165 /** 166 * Initilize oauth request parameters, including: 167 * oauth_consumer_key="0685bd9184jfhq22", 168 * oauth_token="ad180jjd733klru7", 169 * oauth_signature_method="HMAC-SHA1", 170 * oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 171 * oauth_timestamp="137131200", 172 * oauth_nonce="4572616e48616d6d65724c61686176", 173 * oauth_version="1.0" 174 * To access protected resources, oauth_token should be defined 175 * 176 * @param string $url 177 * @param string $token 178 * @param string $http_method 179 * @return array 180 */ 181 public function prepare_oauth_parameters($url, $params, $http_method = 'POST') { 182 if (is_array($params)) { 183 $oauth_params = $params; 184 } else { 185 $oauth_params = array(); 186 } 187 $oauth_params['oauth_version'] = '1.0'; 188 $oauth_params['oauth_nonce'] = $this->get_nonce(); 189 $oauth_params['oauth_timestamp'] = $this->get_timestamp(); 190 $oauth_params['oauth_consumer_key'] = $this->consumer_key; 191 $oauth_params['oauth_signature_method'] = 'HMAC-SHA1'; 192 $oauth_params['oauth_signature'] = $this->sign($http_method, $url, $oauth_params, $this->sign_secret); 193 return $oauth_params; 194 } 195 196 public function setup_oauth_http_header($params) { 197 198 $total = array(); 199 ksort($params); 200 foreach ($params as $k => $v) { 201 $total[] = rawurlencode($k) . '="' . rawurlencode($v).'"'; 202 } 203 $str = implode(', ', $total); 204 $str = 'Authorization: OAuth '.$str; 205 $this->http->setHeader('Expect:'); 206 $this->http->setHeader($str); 207 } 208 209 /** 210 * Sets the options for the next curl request 211 * 212 * @param array $options 213 */ 214 public function setup_oauth_http_options($options) { 215 $this->http_options = $options; 216 } 217 218 /** 219 * Request token for authentication 220 * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret 221 * @return array 222 */ 223 public function request_token() { 224 $this->sign_secret = $this->consumer_secret.'&'; 225 226 if (empty($this->oauth_callback)) { 227 $params = []; 228 } else { 229 $params = ['oauth_callback' => $this->oauth_callback->out(false)]; 230 } 231 232 $params = $this->prepare_oauth_parameters($this->request_token_api, $params, 'GET'); 233 $content = $this->http->get($this->request_token_api, $params, $this->http_options); 234 // Including: 235 // oauth_token 236 // oauth_token_secret 237 $result = $this->parse_result($content); 238 if (empty($result['oauth_token'])) { 239 throw new moodle_exception('oauth1requesttoken', 'core_error', '', null, $content); 240 } 241 // Build oauth authorize url. 242 $result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token']; 243 244 return $result; 245 } 246 247 /** 248 * Set oauth access token for oauth request 249 * @param string $token 250 * @param string $secret 251 */ 252 public function set_access_token($token, $secret) { 253 $this->access_token = $token; 254 $this->access_token_secret = $secret; 255 } 256 257 /** 258 * Request oauth access token from server 259 * @param string $method 260 * @param string $url 261 * @param string $token 262 * @param string $secret 263 */ 264 public function get_access_token($token, $secret, $verifier='') { 265 $this->sign_secret = $this->consumer_secret.'&'.$secret; 266 $params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST'); 267 $this->setup_oauth_http_header($params); 268 // Should never send the callback in this request. 269 unset($params['oauth_callback']); 270 $content = $this->http->post($this->access_token_api, $params, $this->http_options); 271 $keys = $this->parse_result($content); 272 273 if (empty($keys['oauth_token']) || empty($keys['oauth_token_secret'])) { 274 throw new moodle_exception('oauth1accesstoken', 'core_error', '', null, $content); 275 } 276 277 $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']); 278 return $keys; 279 } 280 281 /** 282 * Request oauth protected resources 283 * @param string $method 284 * @param string $url 285 * @param string $token 286 * @param string $secret 287 */ 288 public function request($method, $url, $params=array(), $token='', $secret='') { 289 if (empty($token)) { 290 $token = $this->access_token; 291 } 292 if (empty($secret)) { 293 $secret = $this->access_token_secret; 294 } 295 // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret 296 $this->sign_secret = $this->consumer_secret.'&'.$secret; 297 if (strtolower($method) === 'post' && !empty($params)) { 298 $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) + $params, $method); 299 } else { 300 $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method); 301 } 302 $this->setup_oauth_http_header($oauth_params); 303 $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options)); 304 // reset http header and options to prepare for the next request 305 $this->http->resetHeader(); 306 // return request return value 307 return $content; 308 } 309 310 /** 311 * shortcut to start http get request 312 */ 313 public function get($url, $params=array(), $token='', $secret='') { 314 return $this->request('GET', $url, $params, $token, $secret); 315 } 316 317 /** 318 * shortcut to start http post request 319 */ 320 public function post($url, $params=array(), $token='', $secret='') { 321 return $this->request('POST', $url, $params, $token, $secret); 322 } 323 324 /** 325 * A method to parse oauth response to get oauth_token and oauth_token_secret 326 * @param string $str 327 * @return array 328 */ 329 public function parse_result($str) { 330 if (empty($str)) { 331 throw new moodle_exception('error'); 332 } 333 $parts = explode('&', $str); 334 $result = array(); 335 foreach ($parts as $part){ 336 list($k, $v) = explode('=', $part, 2); 337 $result[urldecode($k)] = urldecode($v); 338 } 339 if (empty($result)) { 340 throw new moodle_exception('error'); 341 } 342 return $result; 343 } 344 345 /** 346 * Set nonce 347 */ 348 function set_nonce($str) { 349 $this->nonce = $str; 350 } 351 /** 352 * Set timestamp 353 */ 354 function set_timestamp($time) { 355 $this->timestamp = $time; 356 } 357 /** 358 * Generate timestamp 359 */ 360 function get_timestamp() { 361 if (!empty($this->timestamp)) { 362 $timestamp = $this->timestamp; 363 unset($this->timestamp); 364 return $timestamp; 365 } 366 return time(); 367 } 368 /** 369 * Generate nonce for oauth request 370 */ 371 function get_nonce() { 372 if (!empty($this->nonce)) { 373 $nonce = $this->nonce; 374 unset($this->nonce); 375 return $nonce; 376 } 377 $mt = microtime(); 378 $rand = mt_rand(); 379 380 return md5($mt . $rand); 381 } 382 } 383 384 /** 385 * OAuth 2.0 Client for using web access tokens. 386 * 387 * http://tools.ietf.org/html/draft-ietf-oauth-v2-22 388 * 389 * @package core 390 * @copyright Dan Poltawski <talktodan@gmail.com> 391 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 392 */ 393 abstract class oauth2_client extends curl { 394 /** @var string $clientid client identifier issued to the client */ 395 private $clientid = ''; 396 /** @var string $clientsecret The client secret. */ 397 private $clientsecret = ''; 398 /** @var moodle_url $returnurl URL to return to after authenticating */ 399 private $returnurl = null; 400 /** @var string $scope of the authentication request */ 401 protected $scope = ''; 402 /** @var stdClass $accesstoken access token object */ 403 protected $accesstoken = null; 404 /** @var string $refreshtoken refresh token string */ 405 protected $refreshtoken = ''; 406 /** @var string $mocknextresponse string */ 407 private $mocknextresponse = ''; 408 /** @var array $upgradedcodes list of upgraded codes in this request */ 409 private static $upgradedcodes = []; 410 /** @var bool basicauth */ 411 protected $basicauth = false; 412 413 /** 414 * Returns the auth url for OAuth 2.0 request 415 * @return string the auth url 416 */ 417 abstract protected function auth_url(); 418 419 /** 420 * Returns the token url for OAuth 2.0 request 421 * @return string the auth url 422 */ 423 abstract protected function token_url(); 424 425 /** 426 * Constructor. 427 * 428 * @param string $clientid 429 * @param string $clientsecret 430 * @param moodle_url $returnurl 431 * @param string $scope 432 */ 433 public function __construct($clientid, $clientsecret, moodle_url $returnurl, $scope) { 434 parent::__construct(); 435 $this->clientid = $clientid; 436 $this->clientsecret = $clientsecret; 437 $this->returnurl = $returnurl; 438 $this->scope = $scope; 439 $this->accesstoken = $this->get_stored_token(); 440 } 441 442 /** 443 * Is the user logged in? Note that if this is called 444 * after the first part of the authorisation flow the token 445 * is upgraded to an accesstoken. 446 * 447 * @return boolean true if logged in 448 */ 449 public function is_logged_in() { 450 // Has the token expired? 451 if (isset($this->accesstoken->expires) && time() >= $this->accesstoken->expires) { 452 $this->store_token(null); 453 return false; 454 } 455 456 // We have a token so we are logged in. 457 if (isset($this->accesstoken->token)) { 458 // Check that the access token has all the requested scopes. 459 $scopemissing = false; 460 $scopecheck = ' ' . $this->accesstoken->scope . ' '; 461 462 $requiredscopes = explode(' ', $this->scope); 463 foreach ($requiredscopes as $requiredscope) { 464 if (strpos($scopecheck, ' ' . $requiredscope . ' ') === false) { 465 $scopemissing = true; 466 break; 467 } 468 } 469 if (!$scopemissing) { 470 return true; 471 } 472 } 473 474 // If we've been passed then authorization code generated by the 475 // authorization server try and upgrade the token to an access token. 476 $code = optional_param('oauth2code', null, PARAM_RAW); 477 // Note - sometimes we may call is_logged_in twice in the same request - we don't want to attempt 478 // to upgrade the same token twice. 479 if ($code && !in_array($code, self::$upgradedcodes) && $this->upgrade_token($code)) { 480 return true; 481 } 482 483 return false; 484 } 485 486 /** 487 * Callback url where the request is returned to. 488 * 489 * @return moodle_url url of callback 490 */ 491 public static function callback_url() { 492 global $CFG; 493 494 return new moodle_url('/admin/oauth2callback.php'); 495 } 496 497 /** 498 * An additional array of url params to pass with a login request. 499 * 500 * @return array of name value pairs. 501 */ 502 public function get_additional_login_parameters() { 503 return []; 504 } 505 506 /** 507 * Returns the login link for this oauth request 508 * 509 * @return moodle_url login url 510 */ 511 public function get_login_url() { 512 513 $callbackurl = self::callback_url(); 514 $defaultparams = [ 515 'client_id' => $this->clientid, 516 'response_type' => 'code', 517 'redirect_uri' => $callbackurl->out(false), 518 'state' => $this->returnurl->out_as_local_url(false), 519 520 ]; 521 if (!empty($this->scope)) { 522 // The scope should only be included if a value is set. 523 // If none provided, the server MUST process the request and provide an appropriate documented response. 524 // See spec https://tools.ietf.org/html/rfc6749#section-3.3 525 $defaultparams['scope'] = $this->scope; 526 } 527 528 $params = array_merge( 529 $defaultparams, 530 $this->get_additional_login_parameters() 531 ); 532 533 return new moodle_url($this->auth_url(), $params); 534 } 535 536 /** 537 * Given an array of name value pairs - build a valid HTTP POST application/x-www-form-urlencoded string. 538 * 539 * @param array $params Name / value pairs. 540 * @return string POST data. 541 */ 542 public function build_post_data($params) { 543 $result = []; 544 foreach ($params as $name => $value) { 545 $result[] = urlencode($name) . '=' . urlencode($value); 546 } 547 return implode('&', $result); 548 } 549 550 /** 551 * Upgrade a authorization token from oauth 2.0 to an access token 552 * 553 * @param string $code the code returned from the oauth authenticaiton 554 * @return boolean true if token is upgraded succesfully 555 */ 556 public function upgrade_token($code) { 557 $callbackurl = self::callback_url(); 558 $params = array('code' => $code, 559 'grant_type' => 'authorization_code', 560 'redirect_uri' => $callbackurl->out(false), 561 ); 562 563 if ($this->basicauth) { 564 $idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret); 565 $this->setHeader('Authorization: Basic ' . base64_encode($idsecret)); 566 } else { 567 $params['client_id'] = $this->clientid; 568 $params['client_secret'] = $this->clientsecret; 569 } 570 571 // Requests can either use http GET or POST. 572 if ($this->use_http_get()) { 573 $response = $this->get($this->token_url(), $params); 574 } else { 575 $response = $this->post($this->token_url(), $this->build_post_data($params)); 576 } 577 578 if ($this->info['http_code'] !== 200) { 579 $debuginfo = !empty($this->error) ? $this->error : $response; 580 throw new moodle_exception('oauth2upgradetokenerror', 'core_error', '', $this->info['http_code'], $debuginfo); 581 } 582 583 $r = json_decode($response); 584 585 if (is_null($r)) { 586 throw new moodle_exception("Could not decode JSON token response"); 587 } 588 589 if (!empty($r->error)) { 590 throw new moodle_exception($r->error . ' ' . $r->error_description); 591 } 592 593 if (!isset($r->access_token)) { 594 return false; 595 } 596 597 if (isset($r->refresh_token)) { 598 $this->refreshtoken = $r->refresh_token; 599 } 600 601 // Store the token an expiry time. 602 $accesstoken = new stdClass; 603 $accesstoken->token = $r->access_token; 604 if (isset($r->expires_in)) { 605 // Expires 10 seconds before actual expiry. 606 $accesstoken->expires = (time() + ($r->expires_in - 10)); 607 } 608 $accesstoken->scope = $this->scope; 609 // Also add the scopes. 610 self::$upgradedcodes[] = $code; 611 $this->store_token($accesstoken); 612 613 return true; 614 } 615 616 /** 617 * Logs out of a oauth request, clearing any stored tokens 618 */ 619 public function log_out() { 620 $this->store_token(null); 621 } 622 623 /** 624 * Make a HTTP request, adding the access token we have 625 * 626 * @param string $url The URL to request 627 * @param array $options 628 * @param mixed $acceptheader mimetype (as string) or false to skip sending an accept header. 629 * @return bool 630 */ 631 protected function request($url, $options = array(), $acceptheader = 'application/json') { 632 $murl = new moodle_url($url); 633 634 if ($this->accesstoken) { 635 if ($this->use_http_get()) { 636 // If using HTTP GET add as a parameter. 637 $murl->param('access_token', $this->accesstoken->token); 638 } else { 639 $this->setHeader('Authorization: Bearer '.$this->accesstoken->token); 640 } 641 } 642 643 if ($acceptheader) { 644 $this->setHeader('Accept: ' . $acceptheader); 645 } 646 647 $response = parent::request($murl->out(false), $options); 648 649 $this->resetHeader(); 650 651 return $response; 652 } 653 654 /** 655 * Multiple HTTP Requests 656 * This function could run multi-requests in parallel. 657 * 658 * @param array $requests An array of files to request 659 * @param array $options An array of options to set 660 * @return array An array of results 661 */ 662 protected function multi($requests, $options = array()) { 663 if ($this->accesstoken) { 664 $this->setHeader('Authorization: Bearer '.$this->accesstoken->token); 665 } 666 return parent::multi($requests, $options); 667 } 668 669 /** 670 * Returns the tokenname for the access_token to be stored 671 * through multiple requests. 672 * 673 * The default implentation is to use the classname combiend 674 * with the scope. 675 * 676 * @return string tokenname for prefernce storage 677 */ 678 protected function get_tokenname() { 679 // This is unusual but should work for most purposes. 680 return get_class($this).'-'.md5($this->scope); 681 } 682 683 /** 684 * Store a token between requests. Currently uses 685 * session named by get_tokenname 686 * 687 * @param stdClass|null $token token object to store or null to clear 688 */ 689 protected function store_token($token) { 690 global $SESSION; 691 692 $this->accesstoken = $token; 693 $name = $this->get_tokenname(); 694 695 if ($token !== null) { 696 $SESSION->{$name} = $token; 697 } else { 698 unset($SESSION->{$name}); 699 } 700 } 701 702 /** 703 * Get a refresh token!!! 704 * 705 * @return string 706 */ 707 public function get_refresh_token() { 708 return $this->refreshtoken; 709 } 710 711 /** 712 * Retrieve a token stored. 713 * 714 * @return stdClass|null token object 715 */ 716 protected function get_stored_token() { 717 global $SESSION; 718 719 $name = $this->get_tokenname(); 720 721 if (isset($SESSION->{$name})) { 722 return $SESSION->{$name}; 723 } 724 725 return null; 726 } 727 728 /** 729 * Get access token. 730 * 731 * This is just a getter to read the private property. 732 * 733 * @return string 734 */ 735 public function get_accesstoken() { 736 return $this->accesstoken; 737 } 738 739 /** 740 * Get the client ID. 741 * 742 * This is just a getter to read the private property. 743 * 744 * @return string 745 */ 746 public function get_clientid() { 747 return $this->clientid; 748 } 749 750 /** 751 * Get the client secret. 752 * 753 * This is just a getter to read the private property. 754 * 755 * @return string 756 */ 757 public function get_clientsecret() { 758 return $this->clientsecret; 759 } 760 761 /** 762 * Should HTTP GET be used instead of POST? 763 * Some APIs do not support POST and want oauth to use 764 * GET instead (with the auth_token passed as a GET param). 765 * 766 * @return bool true if GET should be used 767 */ 768 protected function use_http_get() { 769 return false; 770 } 771 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body