2 // This file is part of Moodle - http://moodle.org/
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.
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');
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())
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/
42 * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com>
43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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*/
53 /** @var string request token url*/
54 protected $request_token_api;
55 /** @var string authorize url*/
56 protected $authorize_url;
57 protected $http_method;
59 protected $access_token_api;
62 /** @var array options to pass to the next curl request */
63 protected $http_options;
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'];
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';
86 $this->request_token_api
= $args['request_token_api'];
89 if (empty($args['authorize_url'])) {
90 $this->authorize_url
= $this->api_root
. '/authorize';
92 $this->authorize_url
= $args['authorize_url'];
95 if (empty($args['access_token_api'])) {
96 $this->access_token_api
= $this->api_root
. '/access_token';
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 if (!empty($args['http_options'])) {
112 $this->http_options
= $args['http_options'];
114 $this->http_options
= array();
119 * Build parameters list:
120 * oauth_consumer_key="0685bd9184jfhq22",
121 * oauth_nonce="4572616e48616d6d65724c61686176",
122 * oauth_token="ad180jjd733klru7",
123 * oauth_signature_method="HMAC-SHA1",
124 * oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
125 * oauth_timestamp="137131200",
126 * oauth_version="1.0"
127 * oauth_verifier="1.0"
128 * @param array $param
131 function get_signable_parameters($params){
136 foreach ($sorted as $k => $v) {
137 if ($k == 'oauth_signature') {
141 $total[] = rawurlencode($k) . '=' . rawurlencode($v);
143 return implode('&', $total);
147 * Create signature for oauth request
149 * @param string $secret
150 * @param array $params
153 public function sign($http_method, $url, $params, $secret) {
155 strtoupper($http_method),
156 preg_replace('/%7E/', '~', rawurlencode($url)),
157 rawurlencode($this->get_signable_parameters($params)),
160 $base_string = implode('&', $sig);
161 $sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true));
166 * Initilize oauth request parameters, including:
167 * oauth_consumer_key="0685bd9184jfhq22",
168 * oauth_token="ad180jjd733klru7",
169 * oauth_signature_method="HMAC-SHA1",
170 * oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
171 * oauth_timestamp="137131200",
172 * oauth_nonce="4572616e48616d6d65724c61686176",
173 * oauth_version="1.0"
174 * To access protected resources, oauth_token should be defined
177 * @param string $token
178 * @param string $http_method
181 public function prepare_oauth_parameters($url, $params, $http_method = 'POST') {
182 if (is_array($params)) {
183 $oauth_params = $params;
185 $oauth_params = array();
187 $oauth_params['oauth_version'] = '1.0';
188 $oauth_params['oauth_nonce'] = $this->get_nonce();
189 $oauth_params['oauth_timestamp'] = $this->get_timestamp();
190 $oauth_params['oauth_consumer_key'] = $this->consumer_key
;
191 $oauth_params['oauth_signature_method'] = 'HMAC-SHA1';
192 $oauth_params['oauth_signature'] = $this->sign($http_method, $url, $oauth_params, $this->sign_secret
);
193 return $oauth_params;
196 public function setup_oauth_http_header($params) {
200 foreach ($params as $k => $v) {
201 $total[] = rawurlencode($k) . '="' . rawurlencode($v).'"';
203 $str = implode(', ', $total);
204 $str = 'Authorization: OAuth '.$str;
205 $this->http
->setHeader('Expect:');
206 $this->http
->setHeader($str);
210 * Sets the options for the next curl request
212 * @param array $options
214 public function setup_oauth_http_options($options) {
215 $this->http_options
= $options;
219 * Request token for authentication
220 * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret
223 public function request_token() {
224 $this->sign_secret
= $this->consumer_secret
.'&';
226 if (empty($this->oauth_callback
)) {
229 $params = ['oauth_callback' => $this->oauth_callback
->out(false)];
232 $params = $this->prepare_oauth_parameters($this->request_token_api
, $params, 'GET');
233 $content = $this->http
->get($this->request_token_api
, $params, $this->http_options
);
236 // oauth_token_secret
237 $result = $this->parse_result($content);
238 if (empty($result['oauth_token'])) {
239 throw new moodle_exception('oauth1requesttoken', 'core_error', '', null, $content);
241 // Build oauth authorize url.
242 $result['authorize_url'] = $this->authorize_url
. '?oauth_token='.$result['oauth_token'];
248 * Set oauth access token for oauth request
249 * @param string $token
250 * @param string $secret
252 public function set_access_token($token, $secret) {
253 $this->access_token
= $token;
254 $this->access_token_secret
= $secret;
258 * Request oauth access token from server
259 * @param string $method
261 * @param string $token
262 * @param string $secret
264 public function get_access_token($token, $secret, $verifier='') {
265 $this->sign_secret
= $this->consumer_secret
.'&'.$secret;
266 $params = $this->prepare_oauth_parameters($this->access_token_api
, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST');
267 $this->setup_oauth_http_header($params);
268 // Should never send the callback in this request.
269 unset($params['oauth_callback']);
270 $content = $this->http
->post($this->access_token_api
, $params, $this->http_options
);
271 $keys = $this->parse_result($content);
273 if (empty($keys['oauth_token']) ||
empty($keys['oauth_token_secret'])) {
274 throw new moodle_exception('oauth1accesstoken', 'core_error', '', null, $content);
277 $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
282 * Request oauth protected resources
283 * @param string $method
285 * @param string $token
286 * @param string $secret
288 public function request($method, $url, $params=array(), $token='', $secret='') {
290 $token = $this->access_token
;
292 if (empty($secret)) {
293 $secret = $this->access_token_secret
;
295 // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret
296 $this->sign_secret
= $this->consumer_secret
.'&'.$secret;
297 if (strtolower($method) === 'post' && !empty($params)) {
298 $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) +
$params, $method);
300 $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
302 $this->setup_oauth_http_header($oauth_params);
303 $content = call_user_func_array(array($this->http
, strtolower($method)), array($url, $params, $this->http_options
));
304 // reset http header and options to prepare for the next request
305 $this->http
->resetHeader();
306 // return request return value
311 * shortcut to start http get request
313 public function get($url, $params=array(), $token='', $secret='') {
314 return $this->request('GET', $url, $params, $token, $secret);
318 * shortcut to start http post request
320 public function post($url, $params=array(), $token='', $secret='') {
321 return $this->request('POST', $url, $params, $token, $secret);
325 * A method to parse oauth response to get oauth_token and oauth_token_secret
329 public function parse_result($str) {
331 throw new moodle_exception('error');
333 $parts = explode('&', $str);
335 foreach ($parts as $part){
336 list($k, $v) = explode('=', $part, 2);
337 $result[urldecode($k)] = urldecode($v);
339 if (empty($result)) {
340 throw new moodle_exception('error');
348 function set_nonce($str) {
354 function set_timestamp($time) {
355 $this->timestamp
= $time;
360 function get_timestamp() {
361 if (!empty($this->timestamp
)) {
362 $timestamp = $this->timestamp
;
363 unset($this->timestamp
);
369 * Generate nonce for oauth request
371 function get_nonce() {
372 if (!empty($this->nonce
)) {
373 $nonce = $this->nonce
;
380 return md5($mt . $rand);
385 * OAuth 2.0 Client for using web access tokens.
387 * http://tools.ietf.org/html/draft-ietf-oauth-v2-22
390 * @copyright Dan Poltawski <talktodan@gmail.com>
391 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
393 abstract class oauth2_client
extends curl
{
394 /** @var string $clientid client identifier issued to the client */
395 private $clientid = '';
396 /** @var string $clientsecret The client secret. */
397 private $clientsecret = '';
398 /** @var moodle_url $returnurl URL to return to after authenticating */
399 private $returnurl = null;
400 /** @var string $scope of the authentication request */
401 protected $scope = '';
402 /** @var stdClass $accesstoken access token object */
403 protected $accesstoken = null;
404 /** @var string $refreshtoken refresh token string */
405 protected $refreshtoken = '';
406 /** @var string $mocknextresponse string */
407 private $mocknextresponse = '';
408 /** @var array $upgradedcodes list of upgraded codes in this request */
409 private static $upgradedcodes = [];
410 /** @var bool basicauth */
411 protected $basicauth = false;
414 * Returns the auth url for OAuth 2.0 request
415 * @return string the auth url
417 abstract protected function auth_url();
420 * Returns the token url for OAuth 2.0 request
421 * @return string the auth url
423 abstract protected function token_url();
428 * @param string $clientid
429 * @param string $clientsecret
430 * @param moodle_url $returnurl
431 * @param string $scope
433 public function __construct($clientid, $clientsecret, moodle_url
$returnurl, $scope) {
434 parent
::__construct();
435 $this->clientid
= $clientid;
436 $this->clientsecret
= $clientsecret;
437 $this->returnurl
= $returnurl;
438 $this->scope
= $scope;
439 $this->accesstoken
= $this->get_stored_token();
443 * Is the user logged in? Note that if this is called
444 * after the first part of the authorisation flow the token
445 * is upgraded to an accesstoken.
447 * @return boolean true if logged in
449 public function is_logged_in() {
450 // Has the token expired?
451 if (isset($this->accesstoken
->expires
) && time() >= $this->accesstoken
->expires
) {
456 // We have a token so we are logged in.
457 if (isset($this->accesstoken
->token
)) {
458 // Check that the access token has all the requested scopes.
459 $scopemissing = false;
460 $scopecheck = ' ' . $this->accesstoken
->scope
. ' ';
462 $requiredscopes = explode(' ', $this->scope
);
463 foreach ($requiredscopes as $requiredscope) {
464 if (strpos($scopecheck, ' ' . $requiredscope . ' ') === false) {
465 $scopemissing = true;
469 if (!$scopemissing) {
474 // If we've been passed then authorization code generated by the
475 // authorization server try and upgrade the token to an access token.
476 $code = optional_param('oauth2code', null, PARAM_RAW
);
477 // Note - sometimes we may call is_logged_in twice in the same request - we don't want to attempt
478 // to upgrade the same token twice.
479 if ($code && !in_array($code, self
::$upgradedcodes) && $this->upgrade_token($code)) {
487 * Callback url where the request is returned to.
489 * @return moodle_url url of callback
491 public static function callback_url() {
494 return new moodle_url('/admin/oauth2callback.php');
498 * An additional array of url params to pass with a login request.
500 * @return array of name value pairs.
502 public function get_additional_login_parameters() {
507 * Returns the login link for this oauth request
509 * @return moodle_url login url
511 public function get_login_url() {
513 $callbackurl = self
::callback_url();
515 'client_id' => $this->clientid
,
516 'response_type' => 'code',
517 'redirect_uri' => $callbackurl->out(false),
518 'state' => $this->returnurl
->out_as_local_url(false),
521 if (!empty($this->scope
)) {
522 // The scope should only be included if a value is set.
523 // If none provided, the server MUST process the request and provide an appropriate documented response.
524 // See spec https://tools.ietf.org/html/rfc6749#section-3.3
525 $defaultparams['scope'] = $this->scope
;
528 $params = array_merge(
530 $this->get_additional_login_parameters()
533 return new moodle_url($this->auth_url(), $params);
537 * Given an array of name value pairs - build a valid HTTP POST application/x-www-form-urlencoded string.
539 * @param array $params Name / value pairs.
540 * @return string POST data.
542 public function build_post_data($params) {
544 foreach ($params as $name => $value) {
545 $result[] = urlencode($name) . '=' . urlencode($value);
547 return implode('&', $result);
551 * Upgrade a authorization token from oauth 2.0 to an access token
553 * @param string $code the code returned from the oauth authenticaiton
554 * @return boolean true if token is upgraded succesfully
556 public function upgrade_token($code) {
557 $callbackurl = self
::callback_url();
558 $params = array('code' => $code,
559 'grant_type' => 'authorization_code',
560 'redirect_uri' => $callbackurl->out(false),
563 if ($this->basicauth
) {
564 $idsecret = urlencode($this->clientid
) . ':' . urlencode($this->clientsecret
);
565 $this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
567 $params['client_id'] = $this->clientid
;
568 $params['client_secret'] = $this->clientsecret
;
571 // Requests can either use http GET or POST.
572 if ($this->use_http_get()) {
573 $response = $this->get($this->token_url(), $params);
575 $response = $this->post($this->token_url(), $this->build_post_data($params));
578 if ($this->info
['http_code'] !== 200) {
579 $debuginfo = !empty($this->error
) ?
$this->error
: $response;
580 throw new moodle_exception('oauth2upgradetokenerror', 'core_error', '', $this->info
['http_code'], $debuginfo);
583 $r = json_decode($response);
586 throw new moodle_exception("Could not decode JSON token response");
589 if (!empty($r->error
)) {
590 throw new moodle_exception($r->error
. ' ' . $r->error_description
);
593 if (!isset($r->access_token
)) {
597 if (isset($r->refresh_token
)) {
598 $this->refreshtoken
= $r->refresh_token
;
601 // Store the token an expiry time.
602 $accesstoken = new stdClass
;
603 $accesstoken->token
= $r->access_token
;
604 if (isset($r->expires_in
)) {
605 // Expires 10 seconds before actual expiry.
606 $accesstoken->expires
= (time() +
($r->expires_in
- 10));
608 $accesstoken->scope
= $this->scope
;
609 // Also add the scopes.
610 self
::$upgradedcodes[] = $code;
611 $this->store_token($accesstoken);
617 * Logs out of a oauth request, clearing any stored tokens
619 public function log_out() {
620 $this->store_token(null);
624 * Make a HTTP request, adding the access token we have
626 * @param string $url The URL to request
627 * @param array $options
628 * @param mixed $acceptheader mimetype (as string) or false to skip sending an accept header.
631 protected function request($url, $options = array(), $acceptheader = 'application/json') {
632 $murl = new moodle_url($url);
634 if ($this->accesstoken
) {
635 if ($this->use_http_get()) {
636 // If using HTTP GET add as a parameter.
637 $murl->param('access_token', $this->accesstoken
->token
);
639 $this->setHeader('Authorization: Bearer '.$this->accesstoken
->token
);
644 $this->setHeader('Accept: ' . $acceptheader);
647 $response = parent
::request($murl->out(false), $options);
649 $this->resetHeader();
655 * Multiple HTTP Requests
656 * This function could run multi-requests in parallel.
658 * @param array $requests An array of files to request
659 * @param array $options An array of options to set
660 * @return array An array of results
662 protected function multi($requests, $options = array()) {
663 if ($this->accesstoken
) {
664 $this->setHeader('Authorization: Bearer '.$this->accesstoken
->token
);
666 return parent
::multi($requests, $options);
670 * Returns the tokenname for the access_token to be stored
671 * through multiple requests.
673 * The default implentation is to use the classname combiend
676 * @return string tokenname for prefernce storage
678 protected function get_tokenname() {
679 // This is unusual but should work for most purposes.
680 return get_class($this).'-'.md5($this->scope
);
684 * Store a token between requests. Currently uses
685 * session named by get_tokenname
687 * @param stdClass|null $token token object to store or null to clear
689 protected function store_token($token) {
692 $this->accesstoken
= $token;
693 $name = $this->get_tokenname();
695 if ($token !== null) {
696 $SESSION->{$name} = $token;
698 unset($SESSION->{$name});
703 * Get a refresh token!!!
707 public function get_refresh_token() {
708 return $this->refreshtoken
;
712 * Retrieve a token stored.
714 * @return stdClass|null token object
716 protected function get_stored_token() {
719 $name = $this->get_tokenname();
721 if (isset($SESSION->{$name})) {
722 return $SESSION->{$name};
731 * This is just a getter to read the private property.
735 public function get_accesstoken() {
736 return $this->accesstoken
;
742 * This is just a getter to read the private property.
746 public function get_clientid() {
747 return $this->clientid
;
751 * Get the client secret.
753 * This is just a getter to read the private property.
757 public function get_clientsecret() {
758 return $this->clientsecret
;
762 * Should HTTP GET be used instead of POST?
763 * Some APIs do not support POST and want oauth to use
764 * GET instead (with the auth_token passed as a GET param).
766 * @return bool true if GET should be used
768 protected function use_http_get() {