Merge branch 'MDL-78457' of https://github.com/paulholden/moodle
[moodle.git] / lib / oauthlib.php
blob009500485815eb53b9279f79bbdad2dfa3d6b0a2
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.
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/>.
18 defined('MOODLE_INTERNAL') || die();
20 require_once($CFG->libdir.'/filelib.php');
22 /**
23 * OAuth helper class
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())
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/
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
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;
78 /**
79 * Contructor for oauth_helper.
80 * Subclass can override construct to build its own $this->http
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
87 function __construct($args) {
88 if (!empty($args['api_root'])) {
89 $this->api_root = $args['api_root'];
90 } else {
91 $this->api_root = '';
93 $this->consumer_key = $args['oauth_consumer_key'];
94 $this->consumer_secret = $args['oauth_consumer_secret'];
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'];
102 if (empty($args['authorize_url'])) {
103 $this->authorize_url = $this->api_root . '/authorize';
104 } else {
105 $this->authorize_url = $args['authorize_url'];
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'];
114 if (!empty($args['oauth_callback'])) {
115 $this->oauth_callback = new moodle_url($args['oauth_callback']);
117 if (!empty($args['access_token'])) {
118 $this->access_token = $args['access_token'];
120 if (!empty($args['access_token_secret'])) {
121 $this->access_token_secret = $args['access_token_secret'];
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();
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
144 function get_signable_parameters($params){
145 $sorted = $params;
146 ksort($sorted);
148 $total = array();
149 foreach ($sorted as $k => $v) {
150 if ($k == 'oauth_signature') {
151 continue;
154 $total[] = rawurlencode($k) . '=' . rawurlencode($v);
156 return implode('&', $total);
160 * Create signature for oauth request
161 * @param string $url
162 * @param string $secret
163 * @param array $params
164 * @return string
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)),
173 $base_string = implode('&', $sig);
174 $sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true));
175 return $sig;
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
189 * @param string $url
190 * @param string $token
191 * @param string $http_method
192 * @return array
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();
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;
209 public function setup_oauth_http_header($params) {
211 $total = array();
212 ksort($params);
213 foreach ($params as $k => $v) {
214 $total[] = rawurlencode($k) . '="' . rawurlencode($v).'"';
216 $str = implode(', ', $total);
217 $str = 'Authorization: OAuth '.$str;
218 $this->http->setHeader('Expect:');
219 $this->http->setHeader($str);
223 * Sets the options for the next curl request
225 * @param array $options
227 public function setup_oauth_http_options($options) {
228 $this->http_options = $options;
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
236 public function request_token() {
237 $this->sign_secret = $this->consumer_secret.'&';
239 if (empty($this->oauth_callback)) {
240 $params = [];
241 } else {
242 $params = ['oauth_callback' => $this->oauth_callback->out(false)];
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);
254 // Build oauth authorize url.
255 $result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token'];
257 return $result;
261 * Set oauth access token for oauth request
262 * @param string $token
263 * @param string $secret
265 public function set_access_token($token, $secret) {
266 $this->access_token = $token;
267 $this->access_token_secret = $secret;
271 * Request oauth access token from server
272 * @param string $method
273 * @param string $url
274 * @param string $token
275 * @param string $secret
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);
286 if (empty($keys['oauth_token']) || empty($keys['oauth_token_secret'])) {
287 throw new moodle_exception('oauth1accesstoken', 'core_error', '', null, $content);
290 $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
291 return $keys;
295 * Request oauth protected resources
296 * @param string $method
297 * @param string $url
298 * @param string $token
299 * @param string $secret
301 public function request($method, $url, $params=array(), $token='', $secret='') {
302 if (empty($token)) {
303 $token = $this->access_token;
305 if (empty($secret)) {
306 $secret = $this->access_token_secret;
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);
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;
324 * shortcut to start http get request
326 public function get($url, $params=array(), $token='', $secret='') {
327 return $this->request('GET', $url, $params, $token, $secret);
331 * shortcut to start http post request
333 public function post($url, $params=array(), $token='', $secret='') {
334 return $this->request('POST', $url, $params, $token, $secret);
338 * A method to parse oauth response to get oauth_token and oauth_token_secret
339 * @param string $str
340 * @return array
342 public function parse_result($str) {
343 if (empty($str)) {
344 throw new moodle_exception('error');
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);
352 if (empty($result)) {
353 throw new moodle_exception('error');
355 return $result;
359 * Set nonce
361 function set_nonce($str) {
362 $this->nonce = $str;
365 * Set timestamp
367 function set_timestamp($time) {
368 $this->timestamp = $time;
371 * Generate timestamp
373 function get_timestamp() {
374 if (!empty($this->timestamp)) {
375 $timestamp = $this->timestamp;
376 unset($this->timestamp);
377 return $timestamp;
379 return time();
382 * Generate nonce for oauth request
384 function get_nonce() {
385 if (!empty($this->nonce)) {
386 $nonce = $this->nonce;
387 unset($this->nonce);
388 return $nonce;
390 $mt = microtime();
391 $rand = mt_rand();
393 return md5($mt . $rand);
398 * OAuth 2.0 Client for using web access tokens.
400 * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
402 * @package core
403 * @copyright Dan Poltawski <talktodan@gmail.com>
404 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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;
427 * Returns the auth url for OAuth 2.0 request
428 * @return string the auth url
430 abstract protected function auth_url();
433 * Returns the token url for OAuth 2.0 request
434 * @return string the auth url
436 abstract protected function token_url();
439 * Constructor.
441 * @param string $clientid
442 * @param string $clientsecret
443 * @param moodle_url $returnurl
444 * @param string $scope
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();
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.
460 * @return boolean true if logged in
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;
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 . ' ';
475 $requiredscopes = explode(' ', $this->scope);
476 foreach ($requiredscopes as $requiredscope) {
477 if (strpos($scopecheck, ' ' . $requiredscope . ' ') === false) {
478 $scopemissing = true;
479 break;
482 if (!$scopemissing) {
483 return true;
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;
496 return false;
500 * Callback url where the request is returned to.
502 * @return moodle_url url of callback
504 public static function callback_url() {
505 global $CFG;
507 return new moodle_url('/admin/oauth2callback.php');
511 * An additional array of url params to pass with a login request.
513 * @return array of name value pairs.
515 public function get_additional_login_parameters() {
516 return [];
520 * Returns the login link for this oauth request
522 * @return moodle_url login url
524 public function get_login_url() {
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),
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;
541 $params = array_merge(
542 $defaultparams,
543 $this->get_additional_login_parameters()
546 return new moodle_url($this->auth_url(), $params);
550 * Given an array of name value pairs - build a valid HTTP POST application/x-www-form-urlencoded string.
552 * @param array $params Name / value pairs.
553 * @return string POST data.
555 public function build_post_data($params) {
556 $result = [];
557 foreach ($params as $name => $value) {
558 $result[] = urlencode($name) . '=' . urlencode($value);
560 return implode('&', $result);
564 * Upgrade a authorization token from oauth 2.0 to an access token
566 * @param string $code the code returned from the oauth authenticaiton
567 * @return boolean true if token is upgraded succesfully
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),
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;
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));
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);
596 $r = json_decode($response);
598 if (is_null($r)) {
599 throw new moodle_exception("Could not decode JSON token response");
602 if (!empty($r->error)) {
603 throw new moodle_exception($r->error . ' ' . $r->error_description);
606 if (!isset($r->access_token)) {
607 return false;
610 if (isset($r->refresh_token)) {
611 $this->refreshtoken = $r->refresh_token;
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));
621 $accesstoken->scope = $this->scope;
622 // Also add the scopes.
623 self::$upgradedcodes[] = $code;
624 $this->store_token($accesstoken);
626 return true;
630 * Logs out of a oauth request, clearing any stored tokens
632 public function log_out() {
633 $this->store_token(null);
637 * Make a HTTP request, adding the access token we have
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
644 protected function request($url, $options = array(), $acceptheader = 'application/json') {
645 $murl = new moodle_url($url);
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);
656 if ($acceptheader) {
657 $this->setHeader('Accept: ' . $acceptheader);
660 $response = parent::request($murl->out(false), $options);
662 $this->resetHeader();
664 return $response;
668 * Multiple HTTP Requests
669 * This function could run multi-requests in parallel.
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
675 protected function multi($requests, $options = array()) {
676 if ($this->accesstoken) {
677 $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
679 return parent::multi($requests, $options);
683 * Returns the tokenname for the access_token to be stored
684 * through multiple requests.
686 * The default implentation is to use the classname combiend
687 * with the scope.
689 * @return string tokenname for prefernce storage
691 protected function get_tokenname() {
692 // This is unusual but should work for most purposes.
693 return get_class($this).'-'.md5($this->scope);
697 * Store a token between requests. Currently uses
698 * session named by get_tokenname
700 * @param stdClass|null $token token object to store or null to clear
702 protected function store_token($token) {
703 global $SESSION;
705 $this->accesstoken = $token;
706 $name = $this->get_tokenname();
708 if ($token !== null) {
709 $SESSION->{$name} = $token;
710 } else {
711 unset($SESSION->{$name});
716 * Get a refresh token!!!
718 * @return string
720 public function get_refresh_token() {
721 return $this->refreshtoken;
725 * Retrieve a token stored.
727 * @return stdClass|null token object
729 protected function get_stored_token() {
730 global $SESSION;
732 $name = $this->get_tokenname();
734 if (isset($SESSION->{$name})) {
735 return $SESSION->{$name};
738 return null;
742 * Get access token object.
744 * This is just a getter to read the private property.
746 * @return stdClass
748 public function get_accesstoken() {
749 return $this->accesstoken;
753 * Get the client ID.
755 * This is just a getter to read the private property.
757 * @return string
759 public function get_clientid() {
760 return $this->clientid;
764 * Get the client secret.
766 * This is just a getter to read the private property.
768 * @return string
770 public function get_clientsecret() {
771 return $this->clientsecret;
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).
779 * @return bool true if GET should be used
781 protected function use_http_get() {
782 return false;