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