MDL-29857 - lib: Add an OAuth 2.0 client
[moodle.git] / lib / oauthlib.php
CommitLineData
3e123368 1<?php
3e123368
DC
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
469fb5d6
DP
17
18defined('MOODLE_INTERNAL') || die();
19
20require_once($CFG->libdir.'/filelib.php');
21
3e123368
DC
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
46class 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
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'];
80
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 }
86
87 if (empty($args['authorize_url'])) {
88 $this->authorize_url = $this->api_root . '/authorize';
89 } else {
90 $this->authorize_url = $args['authorize_url'];
91 }
92
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 }
98
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 }
110
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);
127
128 $total = array();
129 foreach ($sorted as $k => $v) {
130 if ($k == 'oauth_signature') {
131 continue;
132 }
133
134 $total[] = rawurlencode($k) . '=' . rawurlencode($v);
135 }
136 return implode('&', $total);
137 }
138
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 );
152
153 $base_string = implode('&', $sig);
154 $sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true));
155 return $sig;
156 }
157
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 }
191
192 public function setup_oauth_http_header($params) {
193
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 }
204
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 }
233
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 }
243
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 }
260
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 }
282
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 }
289
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 }
296
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 }
317
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();
352
353 return md5($mt . $rand);
354 }
355}
469fb5d6
DP
356
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 */
366abstract 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;
377
378 /**
379 * Returns the auth url for OAuth 2.0 request
380 * @return string the auth url
381 */
382 abstract protected function auth_url();
383
384 /**
385 * Returns the token url for OAuth 2.0 request
386 * @return string the auth url
387 */
388 abstract protected function token_url();
389
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 }
406
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 }
420
421 // We have a token so we are logged in.
422 if (isset($this->accesstoken->token)) {
423 return true;
424 }
425
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 }
432
433 return false;
434 }
435
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;
443
444 return new moodle_url('/admin/oauth2callback.php');
445 }
446
447 /**
448 * Returns the login link for this oauth request
449 *
450 * @return moodle_url login url
451 */
452 public function get_login_url() {
453
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 ));
462
463 return $url;
464 }
465
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 );
480
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 }
487
488 if (!$this->info['http_code'] === 200) {
489 throw new moodle_exception('Could not upgrade oauth token');
490 }
491
492 $r = json_decode($response);
493
494 if (!isset($r->access_token)) {
495 return false;
496 }
497
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);
503
504 return true;
505 }
506
507 /**
508 * Logs out of a oauth request, clearing any stored tokens
509 */
510 public function log_out() {
511 $this->store_token(null);
512 }
513
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);
523
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 }
532
533 return parent::request($murl->out(false), $options);
534 }
535
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 }
550
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 }
564
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;
573
574 $this->accesstoken = $token;
575 $name = $this->get_tokenname();
576
577 if ($token !== null) {
578 $SESSION->{$name} = $token;
579 } else {
580 unset($SESSION->{$name});
581 }
582 }
583
584 /**
585 * Retrieve a token stored.
586 *
587 * @return stdClass|null token object
588 */
589 protected function get_stored_token() {
590 global $SESSION;
591
592 $name = $this->get_tokenname();
593
594 if (isset($SESSION->{$name})) {
595 return $SESSION->{$name};
596 }
597
598 return null;
599 }
600
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 }
611}