Merge branch 'MDL-40255_M25' of git://github.com/lazydaisy/moodle into MOODLE_25_STABLE
[moodle.git] / lib / oauthlib.php
blob6fc448312ea347d116cc6fea560d50d772b308c4
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;
65 /**
66 * Contructor for oauth_helper.
67 * Subclass can override construct to build its own $this->http
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
74 function __construct($args) {
75 if (!empty($args['api_root'])) {
76 $this->api_root = $args['api_root'];
77 } else {
78 $this->api_root = '';
80 $this->consumer_key = $args['oauth_consumer_key'];
81 $this->consumer_secret = $args['oauth_consumer_secret'];
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'];
89 if (empty($args['authorize_url'])) {
90 $this->authorize_url = $this->api_root . '/authorize';
91 } else {
92 $this->authorize_url = $args['authorize_url'];
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'];
101 if (!empty($args['oauth_callback'])) {
102 $this->oauth_callback = new moodle_url($args['oauth_callback']);
104 if (!empty($args['access_token'])) {
105 $this->access_token = $args['access_token'];
107 if (!empty($args['access_token_secret'])) {
108 $this->access_token_secret = $args['access_token_secret'];
110 $this->http = new curl(array('debug'=>false));
111 $this->http_options = array();
115 * Build parameters list:
116 * oauth_consumer_key="0685bd9184jfhq22",
117 * oauth_nonce="4572616e48616d6d65724c61686176",
118 * oauth_token="ad180jjd733klru7",
119 * oauth_signature_method="HMAC-SHA1",
120 * oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
121 * oauth_timestamp="137131200",
122 * oauth_version="1.0"
123 * oauth_verifier="1.0"
124 * @param array $param
125 * @return string
127 function get_signable_parameters($params){
128 $sorted = $params;
129 ksort($sorted);
131 $total = array();
132 foreach ($sorted as $k => $v) {
133 if ($k == 'oauth_signature') {
134 continue;
137 $total[] = rawurlencode($k) . '=' . rawurlencode($v);
139 return implode('&', $total);
143 * Create signature for oauth request
144 * @param string $url
145 * @param string $secret
146 * @param array $params
147 * @return string
149 public function sign($http_method, $url, $params, $secret) {
150 $sig = array(
151 strtoupper($http_method),
152 preg_replace('/%7E/', '~', rawurlencode($url)),
153 rawurlencode($this->get_signable_parameters($params)),
156 $base_string = implode('&', $sig);
157 $sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true));
158 return $sig;
162 * Initilize oauth request parameters, including:
163 * oauth_consumer_key="0685bd9184jfhq22",
164 * oauth_token="ad180jjd733klru7",
165 * oauth_signature_method="HMAC-SHA1",
166 * oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
167 * oauth_timestamp="137131200",
168 * oauth_nonce="4572616e48616d6d65724c61686176",
169 * oauth_version="1.0"
170 * To access protected resources, oauth_token should be defined
172 * @param string $url
173 * @param string $token
174 * @param string $http_method
175 * @return array
177 public function prepare_oauth_parameters($url, $params, $http_method = 'POST') {
178 if (is_array($params)) {
179 $oauth_params = $params;
180 } else {
181 $oauth_params = array();
183 $oauth_params['oauth_version'] = '1.0';
184 $oauth_params['oauth_nonce'] = $this->get_nonce();
185 $oauth_params['oauth_timestamp'] = $this->get_timestamp();
186 $oauth_params['oauth_consumer_key'] = $this->consumer_key;
187 if (!empty($this->oauth_callback)) {
188 $oauth_params['oauth_callback'] = $this->oauth_callback->out(false);
190 $oauth_params['oauth_signature_method'] = 'HMAC-SHA1';
191 $oauth_params['oauth_signature'] = $this->sign($http_method, $url, $oauth_params, $this->sign_secret);
192 return $oauth_params;
195 public function setup_oauth_http_header($params) {
197 $total = array();
198 ksort($params);
199 foreach ($params as $k => $v) {
200 $total[] = rawurlencode($k) . '="' . rawurlencode($v).'"';
202 $str = implode(', ', $total);
203 $str = 'Authorization: OAuth '.$str;
204 $this->http->setHeader('Expect:');
205 $this->http->setHeader($str);
209 * Sets the options for the next curl request
211 * @param array $options
213 public function setup_oauth_http_options($options) {
214 $this->http_options = $options;
218 * Request token for authentication
219 * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret
220 * @return array
222 public function request_token() {
223 $this->sign_secret = $this->consumer_secret.'&';
224 $params = $this->prepare_oauth_parameters($this->request_token_api, array(), 'GET');
225 $content = $this->http->get($this->request_token_api, $params, $this->http_options);
226 // Including:
227 // oauth_token
228 // oauth_token_secret
229 $result = $this->parse_result($content);
230 if (empty($result['oauth_token'])) {
231 // failed
232 var_dump($result);
233 exit;
235 // build oauth authrize url
236 if (!empty($this->oauth_callback)) {
237 // url must be rawurlencode
238 $result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token'].'&oauth_callback='.rawurlencode($this->oauth_callback->out(false));
239 } else {
240 // no callback
241 $result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token'];
243 return $result;
247 * Set oauth access token for oauth request
248 * @param string $token
249 * @param string $secret
251 public function set_access_token($token, $secret) {
252 $this->access_token = $token;
253 $this->access_token_secret = $secret;
257 * Request oauth access token from server
258 * @param string $method
259 * @param string $url
260 * @param string $token
261 * @param string $secret
263 public function get_access_token($token, $secret, $verifier='') {
264 $this->sign_secret = $this->consumer_secret.'&'.$secret;
265 $params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST');
266 $this->setup_oauth_http_header($params);
267 // Should never send the callback in this request.
268 unset($params['oauth_callback']);
269 $content = $this->http->post($this->access_token_api, $params, $this->http_options);
270 $keys = $this->parse_result($content);
271 $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
272 return $keys;
276 * Request oauth protected resources
277 * @param string $method
278 * @param string $url
279 * @param string $token
280 * @param string $secret
282 public function request($method, $url, $params=array(), $token='', $secret='') {
283 if (empty($token)) {
284 $token = $this->access_token;
286 if (empty($secret)) {
287 $secret = $this->access_token_secret;
289 // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret
290 $this->sign_secret = $this->consumer_secret.'&'.$secret;
291 if (strtolower($method) === 'post' && !empty($params)) {
292 $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) + $params, $method);
293 } else {
294 $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
296 $this->setup_oauth_http_header($oauth_params);
297 $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options));
298 // reset http header and options to prepare for the next request
299 $this->http->resetHeader();
300 // return request return value
301 return $content;
305 * shortcut to start http get request
307 public function get($url, $params=array(), $token='', $secret='') {
308 return $this->request('GET', $url, $params, $token, $secret);
312 * shortcut to start http post request
314 public function post($url, $params=array(), $token='', $secret='') {
315 return $this->request('POST', $url, $params, $token, $secret);
319 * A method to parse oauth response to get oauth_token and oauth_token_secret
320 * @param string $str
321 * @return array
323 public function parse_result($str) {
324 if (empty($str)) {
325 throw new moodle_exception('error');
327 $parts = explode('&', $str);
328 $result = array();
329 foreach ($parts as $part){
330 list($k, $v) = explode('=', $part, 2);
331 $result[urldecode($k)] = urldecode($v);
333 if (empty($result)) {
334 throw new moodle_exception('error');
336 return $result;
340 * Set nonce
342 function set_nonce($str) {
343 $this->nonce = $str;
346 * Set timestamp
348 function set_timestamp($time) {
349 $this->timestamp = $time;
352 * Generate timestamp
354 function get_timestamp() {
355 if (!empty($this->timestamp)) {
356 $timestamp = $this->timestamp;
357 unset($this->timestamp);
358 return $timestamp;
360 return time();
363 * Generate nonce for oauth request
365 function get_nonce() {
366 if (!empty($this->nonce)) {
367 $nonce = $this->nonce;
368 unset($this->nonce);
369 return $nonce;
371 $mt = microtime();
372 $rand = mt_rand();
374 return md5($mt . $rand);
379 * OAuth 2.0 Client for using web access tokens.
381 * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
383 * @package core
384 * @copyright Dan Poltawski <talktodan@gmail.com>
385 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
387 abstract class oauth2_client extends curl {
388 /** var string client identifier issued to the client */
389 private $clientid = '';
390 /** var string The client secret. */
391 private $clientsecret = '';
392 /** var moodle_url URL to return to after authenticating */
393 private $returnurl = null;
394 /** var string scope of the authentication request */
395 private $scope = '';
396 /** var stdClass access token object */
397 private $accesstoken = null;
400 * Returns the auth url for OAuth 2.0 request
401 * @return string the auth url
403 abstract protected function auth_url();
406 * Returns the token url for OAuth 2.0 request
407 * @return string the auth url
409 abstract protected function token_url();
412 * Constructor.
414 * @param string $clientid
415 * @param string $clientsecret
416 * @param moodle_url $returnurl
417 * @param string $scope
419 public function __construct($clientid, $clientsecret, moodle_url $returnurl, $scope) {
420 parent::__construct();
421 $this->clientid = $clientid;
422 $this->clientsecret = $clientsecret;
423 $this->returnurl = $returnurl;
424 $this->scope = $scope;
425 $this->accesstoken = $this->get_stored_token();
429 * Is the user logged in? Note that if this is called
430 * after the first part of the authorisation flow the token
431 * is upgraded to an accesstoken.
433 * @return boolean true if logged in
435 public function is_logged_in() {
436 // Has the token expired?
437 if (isset($this->accesstoken->expires) && time() >= $this->accesstoken->expires) {
438 $this->log_out();
439 return false;
442 // We have a token so we are logged in.
443 if (isset($this->accesstoken->token)) {
444 return true;
447 // If we've been passed then authorization code generated by the
448 // authorization server try and upgrade the token to an access token.
449 $code = optional_param('oauth2code', null, PARAM_RAW);
450 if ($code && $this->upgrade_token($code)) {
451 return true;
454 return false;
458 * Callback url where the request is returned to.
460 * @return moodle_url url of callback
462 public static function callback_url() {
463 global $CFG;
465 return new moodle_url('/admin/oauth2callback.php');
469 * Returns the login link for this oauth request
471 * @return moodle_url login url
473 public function get_login_url() {
475 $callbackurl = self::callback_url();
476 $url = new moodle_url($this->auth_url(),
477 array('client_id' => $this->clientid,
478 'response_type' => 'code',
479 'redirect_uri' => $callbackurl->out(false),
480 'state' => $this->returnurl->out_as_local_url(false),
481 'scope' => $this->scope,
484 return $url;
488 * Upgrade a authorization token from oauth 2.0 to an access token
490 * @param string $code the code returned from the oauth authenticaiton
491 * @return boolean true if token is upgraded succesfully
493 public function upgrade_token($code) {
494 $callbackurl = self::callback_url();
495 $params = array('client_id' => $this->clientid,
496 'client_secret' => $this->clientsecret,
497 'grant_type' => 'authorization_code',
498 'code' => $code,
499 'redirect_uri' => $callbackurl->out(false),
502 // Requests can either use http GET or POST.
503 if ($this->use_http_get()) {
504 $response = $this->get($this->token_url(), $params);
505 } else {
506 $response = $this->post($this->token_url(), $params);
509 if (!$this->info['http_code'] === 200) {
510 throw new moodle_exception('Could not upgrade oauth token');
513 $r = json_decode($response);
515 if (!isset($r->access_token)) {
516 return false;
519 // Store the token an expiry time.
520 $accesstoken = new stdClass;
521 $accesstoken->token = $r->access_token;
522 $accesstoken->expires = (time() + ($r->expires_in - 10)); // Expires 10 seconds before actual expiry.
523 $this->store_token($accesstoken);
525 return true;
529 * Logs out of a oauth request, clearing any stored tokens
531 public function log_out() {
532 $this->store_token(null);
536 * Make a HTTP request, adding the access token we have
538 * @param string $url The URL to request
539 * @param array $options
540 * @return bool
542 protected function request($url, $options = array()) {
543 $murl = new moodle_url($url);
545 if ($this->accesstoken) {
546 if ($this->use_http_get()) {
547 // If using HTTP GET add as a parameter.
548 $murl->param('access_token', $this->accesstoken->token);
549 } else {
550 $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
554 return parent::request($murl->out(false), $options);
558 * Multiple HTTP Requests
559 * This function could run multi-requests in parallel.
561 * @param array $requests An array of files to request
562 * @param array $options An array of options to set
563 * @return array An array of results
565 protected function multi($requests, $options = array()) {
566 if ($this->accesstoken) {
567 $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
569 return parent::multi($requests, $options);
573 * Returns the tokenname for the access_token to be stored
574 * through multiple requests.
576 * The default implentation is to use the classname combiend
577 * with the scope.
579 * @return string tokenname for prefernce storage
581 protected function get_tokenname() {
582 // This is unusual but should work for most purposes.
583 return get_class($this).'-'.md5($this->scope);
587 * Store a token between requests. Currently uses
588 * session named by get_tokenname
590 * @param stdClass|null $token token object to store or null to clear
592 protected function store_token($token) {
593 global $SESSION;
595 $this->accesstoken = $token;
596 $name = $this->get_tokenname();
598 if ($token !== null) {
599 $SESSION->{$name} = $token;
600 } else {
601 unset($SESSION->{$name});
606 * Retrieve a token stored.
608 * @return stdClass|null token object
610 protected function get_stored_token() {
611 global $SESSION;
613 $name = $this->get_tokenname();
615 if (isset($SESSION->{$name})) {
616 return $SESSION->{$name};
619 return null;
623 * Should HTTP GET be used instead of POST?
624 * Some APIs do not support POST and want oauth to use
625 * GET instead (with the auth_token passed as a GET param).
627 * @return bool true if GET should be used
629 protected function use_http_get() {
630 return false;