Merge branch 'install_master' of git://github.com/amosbot/moodle
[moodle.git] / lib / oauthlib.php
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/>.
18 defined('MOODLE_INTERNAL') || die();
20 require_once($CFG->libdir.'/filelib.php');
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  */
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;
63     /**
64      * Contructor for oauth_helper.
65      * Subclass can override construct to build its own $this->http
66      *
67      * @param array $args requires at least three keys, oauth_consumer_key
68      *                    oauth_consumer_secret and api_root, oauth_helper will
69      *                    guess request_token_api, authrize_url and access_token_api
70      *                    based on api_root, but it not always works
71      */
72     function __construct($args) {
73         if (!empty($args['api_root'])) {
74             $this->api_root = $args['api_root'];
75         } else {
76             $this->api_root = '';
77         }
78         $this->consumer_key = $args['oauth_consumer_key'];
79         $this->consumer_secret = $args['oauth_consumer_secret'];
81         if (empty($args['request_token_api'])) {
82             $this->request_token_api = $this->api_root . '/request_token';
83         } else {
84             $this->request_token_api = $args['request_token_api'];
85         }
87         if (empty($args['authorize_url'])) {
88             $this->authorize_url = $this->api_root . '/authorize';
89         } else {
90             $this->authorize_url = $args['authorize_url'];
91         }
93         if (empty($args['access_token_api'])) {
94             $this->access_token_api = $this->api_root . '/access_token';
95         } else {
96             $this->access_token_api = $args['access_token_api'];
97         }
99         if (!empty($args['oauth_callback'])) {
100             $this->oauth_callback = new moodle_url($args['oauth_callback']);
101         }
102         if (!empty($args['access_token'])) {
103             $this->access_token = $args['access_token'];
104         }
105         if (!empty($args['access_token_secret'])) {
106             $this->access_token_secret = $args['access_token_secret'];
107         }
108         $this->http = new curl(array('debug'=>false));
109     }
111     /**
112      * Build parameters list:
113      *    oauth_consumer_key="0685bd9184jfhq22",
114      *    oauth_nonce="4572616e48616d6d65724c61686176",
115      *    oauth_token="ad180jjd733klru7",
116      *    oauth_signature_method="HMAC-SHA1",
117      *    oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
118      *    oauth_timestamp="137131200",
119      *    oauth_version="1.0"
120      *    oauth_verifier="1.0"
121      * @param array $param
122      * @return string
123      */
124     function get_signable_parameters($params){
125         $sorted = $params;
126         ksort($sorted);
128         $total = array();
129         foreach ($sorted as $k => $v) {
130             if ($k == 'oauth_signature') {
131                 continue;
132             }
134             $total[] = rawurlencode($k) . '=' . rawurlencode($v);
135         }
136         return implode('&', $total);
137     }
139     /**
140      * Create signature for oauth request
141      * @param string $url
142      * @param string $secret
143      * @param array $params
144      * @return string
145      */
146     public function sign($http_method, $url, $params, $secret) {
147         $sig = array(
148             strtoupper($http_method),
149             preg_replace('/%7E/', '~', rawurlencode($url)),
150             rawurlencode($this->get_signable_parameters($params)),
151         );
153         $base_string = implode('&', $sig);
154         $sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true));
155         return $sig;
156     }
158     /**
159      * Initilize oauth request parameters, including:
160      *    oauth_consumer_key="0685bd9184jfhq22",
161      *    oauth_token="ad180jjd733klru7",
162      *    oauth_signature_method="HMAC-SHA1",
163      *    oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
164      *    oauth_timestamp="137131200",
165      *    oauth_nonce="4572616e48616d6d65724c61686176",
166      *    oauth_version="1.0"
167      * To access protected resources, oauth_token should be defined
168      *
169      * @param string $url
170      * @param string $token
171      * @param string $http_method
172      * @return array
173      */
174     public function prepare_oauth_parameters($url, $params, $http_method = 'POST') {
175         if (is_array($params)) {
176             $oauth_params = $params;
177         } else {
178             $oauth_params = array();
179         }
180         $oauth_params['oauth_version']      = '1.0';
181         $oauth_params['oauth_nonce']        = $this->get_nonce();
182         $oauth_params['oauth_timestamp']    = $this->get_timestamp();
183         $oauth_params['oauth_consumer_key'] = $this->consumer_key;
184         if (!empty($this->oauth_callback)) {
185             $oauth_params['oauth_callback'] = $this->oauth_callback->out(false);
186         }
187         $oauth_params['oauth_signature_method'] = 'HMAC-SHA1';
188         $oauth_params['oauth_signature']        = $this->sign($http_method, $url, $oauth_params, $this->sign_secret);
189         return $oauth_params;
190     }
192     public function setup_oauth_http_header($params) {
194         $total = array();
195         ksort($params);
196         foreach ($params as $k => $v) {
197             $total[] = rawurlencode($k) . '="' . rawurlencode($v).'"';
198         }
199         $str = implode(', ', $total);
200         $str = 'Authorization: OAuth '.$str;
201         $this->http->setHeader('Expect:');
202         $this->http->setHeader($str);
203     }
205     /**
206      * Request token for authentication
207      * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret
208      * @return array
209      */
210     public function request_token() {
211         $this->sign_secret = $this->consumer_secret.'&';
212         $params = $this->prepare_oauth_parameters($this->request_token_api, array(), 'GET');
213         $content = $this->http->get($this->request_token_api, $params);
214         // Including:
215         //     oauth_token
216         //     oauth_token_secret
217         $result = $this->parse_result($content);
218         if (empty($result['oauth_token'])) {
219             // failed
220             var_dump($result);
221             exit;
222         }
223         // build oauth authrize url
224         if (!empty($this->oauth_callback)) {
225             // url must be rawurlencode
226             $result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token'].'&oauth_callback='.rawurlencode($this->oauth_callback->out(false));
227         } else {
228             // no callback
229             $result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token'];
230         }
231         return $result;
232     }
234     /**
235      * Set oauth access token for oauth request
236      * @param string $token
237      * @param string $secret
238      */
239     public function set_access_token($token, $secret) {
240         $this->access_token = $token;
241         $this->access_token_secret = $secret;
242     }
244     /**
245      * Request oauth access token from server
246      * @param string $method
247      * @param string $url
248      * @param string $token
249      * @param string $secret
250      */
251     public function get_access_token($token, $secret, $verifier='') {
252         $this->sign_secret = $this->consumer_secret.'&'.$secret;
253         $params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST');
254         $this->setup_oauth_http_header($params);
255         $content = $this->http->post($this->access_token_api, $params);
256         $keys = $this->parse_result($content);
257         $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
258         return $keys;
259     }
261     /**
262      * Request oauth protected resources
263      * @param string $method
264      * @param string $url
265      * @param string $token
266      * @param string $secret
267      */
268     public function request($method, $url, $params=array(), $token='', $secret='') {
269         if (empty($token)) {
270             $token = $this->access_token;
271         }
272         if (empty($secret)) {
273             $secret = $this->access_token_secret;
274         }
275         // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret
276         $this->sign_secret = $this->consumer_secret.'&'.$secret;
277         $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
278         $this->setup_oauth_http_header($oauth_params);
279         $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params));
280         return $content;
281     }
283     /**
284      * shortcut to start http get request
285      */
286     public function get($url, $params=array(), $token='', $secret='') {
287         return $this->request('GET', $url, $params, $token, $secret);
288     }
290     /**
291      * shortcut to start http post request
292      */
293     public function post($url, $params=array(), $token='', $secret='') {
294         return $this->request('POST', $url, $params, $token, $secret);
295     }
297     /**
298      * A method to parse oauth response to get oauth_token and oauth_token_secret
299      * @param string $str
300      * @return array
301      */
302     public function parse_result($str) {
303         if (empty($str)) {
304             throw new moodle_exception('error');
305         }
306         $parts = explode('&', $str);
307         $result = array();
308         foreach ($parts as $part){
309             list($k, $v) = explode('=', $part, 2);
310             $result[urldecode($k)] = urldecode($v);
311         }
312         if (empty($result)) {
313             throw new moodle_exception('error');
314         }
315         return $result;
316     }
318     /**
319      * Set nonce
320      */
321     function set_nonce($str) {
322         $this->nonce = $str;
323     }
324     /**
325      * Set timestamp
326      */
327     function set_timestamp($time) {
328         $this->timestamp = $time;
329     }
330     /**
331      * Generate timestamp
332      */
333     function get_timestamp() {
334         if (!empty($this->timestamp)) {
335             $timestamp = $this->timestamp;
336             unset($this->timestamp);
337             return $timestamp;
338         }
339         return time();
340     }
341     /**
342      * Generate nonce for oauth request
343      */
344     function get_nonce() {
345         if (!empty($this->nonce)) {
346             $nonce = $this->nonce;
347             unset($this->nonce);
348             return $nonce;
349         }
350         $mt = microtime();
351         $rand = mt_rand();
353         return md5($mt . $rand);
354     }
357 /**
358  * OAuth 2.0 Client for using web access tokens.
359  *
360  * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
361  *
362  * @package   core
363  * @copyright Dan Poltawski <talktodan@gmail.com>
364  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
365  */
366 abstract class oauth2_client extends curl {
367     /** var string client identifier issued to the client */
368     private $clientid = '';
369     /** var string The client secret. */
370     private $clientsecret = '';
371     /** var string URL to return to after authenticating */
372     private $returnurl = '';
373     /** var string scope of the authentication request */
374     private $scope = '';
375     /** var stdClass access token object */
376     private $accesstoken = null;
378     /**
379      * Returns the auth url for OAuth 2.0 request
380      * @return string the auth url
381      */
382     abstract protected function auth_url();
384     /**
385      * Returns the token url for OAuth 2.0 request
386      * @return string the auth url
387      */
388     abstract protected function token_url();
390     /**
391      * Constructor.
392      *
393      * @param string $clientid
394      * @param string $clientsecret
395      * @param string $returnurl
396      * @param string $scope
397      */
398     public function __construct($clientid, $clientsecret, $returnurl, $scope) {
399         parent::__construct();
400         $this->clientid = $clientid;
401         $this->clientsecret = $clientsecret;
402         $this->returnurl = $returnurl;
403         $this->scope = $scope;
404         $this->accesstoken = $this->get_stored_token();
405     }
407     /**
408      * Is the user logged in? Note that if this is called
409      * after the first part of the authorisation flow the token
410      * is upgraded to an accesstoken.
411      *
412      * @return boolean true if logged in
413      */
414     public function is_logged_in() {
415         // Has the token expired?
416         if (isset($this->accesstoken->expires) && time() >= $this->accesstoken->expires) {
417             $this->log_out();
418             return false;
419         }
421         // We have a token so we are logged in.
422         if (isset($this->accesstoken->token)) {
423             return true;
424         }
426         // If we've been passed then authorization code generated by the
427         // authorization server try and upgrade the token to an access token.
428         $code = optional_param('code', null, PARAM_RAW);
429         if ($code && $this->upgrade_token($code)) {
430             return true;
431         }
433         return false;
434     }
436     /**
437      * Callback url where the request is returned to.
438      *
439      * @return moodle_url url of callback
440      */
441     public static function callback_url() {
442         global $CFG;
444         return new moodle_url('/admin/oauth2callback.php');
445     }
447     /**
448      * Returns the login link for this oauth request
449      *
450      * @return moodle_url login url
451      */
452     public function get_login_url() {
454         $callbackurl = self::callback_url();
455         $url = new moodle_url($this->auth_url(),
456                         array('client_id' => $this->clientid,
457                               'response_type' => 'code',
458                               'redirect_uri' => $callbackurl->out(false),
459                               'state' => $this->returnurl,
460                               'scope' => $this->scope,
461                           ));
463         return $url;
464     }
466     /**
467      * Upgrade a authorization token from oauth 2.0 to an access token
468      *
469      * @param string $code the code returned from the oauth authenticaiton
470      * @return boolean true if token is upgraded succesfully
471      */
472     public function upgrade_token($code) {
473         $callbackurl = self::callback_url();
474         $params = array('client_id' => $this->clientid,
475             'client_secret' => $this->clientsecret,
476             'grant_type' => 'authorization_code',
477             'code' => $code,
478             'redirect_uri' => $callbackurl->out(false),
479         );
481         // Requests can either use http GET or POST.
482         if ($this->use_http_get()) {
483             $response = $this->get($this->token_url(), $params);
484         } else {
485             $response = $this->post($this->token_url(), $params);
486         }
488         if (!$this->info['http_code'] === 200) {
489             throw new moodle_exception('Could not upgrade oauth token');
490         }
492         $r = json_decode($response);
494         if (!isset($r->access_token)) {
495             return false;
496         }
498         // Store the token an expiry time.
499         $accesstoken = new stdClass;
500         $accesstoken->token = $r->access_token;
501         $accesstoken->expires = (time() + ($r->expires_in - 10)); // Expires 10 seconds before actual expiry.
502         $this->store_token($accesstoken);
504         return true;
505     }
507     /**
508      * Logs out of a oauth request, clearing any stored tokens
509      */
510     public function log_out() {
511         $this->store_token(null);
512     }
514     /**
515      * Make a HTTP request, adding the access token we have
516      *
517      * @param string $url The URL to request
518      * @param array $options
519      * @return bool
520      */
521     protected function request($url, $options = array()) {
522         $murl = new moodle_url($url);
524         if ($this->accesstoken) {
525             if ($this->use_http_get()) {
526                 // If using HTTP GET add as a parameter.
527                 $murl->param('access_token', $this->accesstoken->token);
528             } else {
529                 $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
530             }
531         }
533         return parent::request($murl->out(false), $options);
534     }
536     /**
537      * Multiple HTTP Requests
538      * This function could run multi-requests in parallel.
539      *
540      * @param array $requests An array of files to request
541      * @param array $options An array of options to set
542      * @return array An array of results
543      */
544     protected function multi($requests, $options = array()) {
545         if ($this->accesstoken) {
546             $this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
547         }
548         return parent::multi($requests, $options);
549     }
551     /**
552      * Returns the tokenname for the access_token to be stored
553      * through multiple requests.
554      *
555      * The default implentation is to use the classname combiend
556      * with the scope.
557      *
558      * @return string tokenname for prefernce storage
559      */
560     protected function get_tokenname() {
561         // This is unusual but should work for most purposes.
562         return get_class($this).'-'.md5($this->scope);
563     }
565     /**
566      * Store a token between requests. Currently uses
567      * session named by get_tokenname
568      *
569      * @param stdClass|null $token token object to store or null to clear
570      */
571     protected function store_token($token) {
572         global $SESSION;
574         $this->accesstoken = $token;
575         $name = $this->get_tokenname();
577         if ($token !== null) {
578             $SESSION->{$name} = $token;
579         } else {
580             unset($SESSION->{$name});
581         }
582     }
584     /**
585      * Retrieve a token stored.
586      *
587      * @return stdClass|null token object
588      */
589     protected function get_stored_token() {
590         global $SESSION;
592         $name = $this->get_tokenname();
594         if (isset($SESSION->{$name})) {
595             return $SESSION->{$name};
596         }
598         return null;
599     }
601     /**
602      * Should HTTP GET be used instead of POST?
603      * Some APIs do not support POST and want oauth to use
604      * GET instead (with the auth_token passed as a GET param).
605      *
606      * @return bool true if GET should be used
607      */
608     protected function use_http_get() {
609         return false;
610     }