moodle/lib/oauthlib.php
Artur Neumann d4709d817e
MDL-79487 oauth2: do not delete refresh token
when `log_out` is called from `\core\oauth2\client` it will delete the refresh token,
what it actually needs to use it to get a new access token
actually logging out is not needed here, the only thing we need to make sure is,
the invalid access token is removed from the session
that is done by storing `null`
2023-09-28 15:57:12 +05:45

785 lines
25 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/filelib.php');
/**
* OAuth helper class
*
* 1. You can extends oauth_helper to add specific functions, such as twitter extends oauth_helper
* 2. Call request_token method to get oauth_token and oauth_token_secret, and redirect user to authorize_url,
* developer needs to store oauth_token and oauth_token_secret somewhere, we will use them to request
* access token later on
* 3. User approved the request, and get back to moodle
* 4. Call get_access_token, it takes previous oauth_token and oauth_token_secret as arguments, oauth_token
* will be used in OAuth request, oauth_token_secret will be used to bulid signature, this method will
* return access_token and access_secret, store these two values in database or session
* 5. Now you can access oauth protected resources by access_token and access_secret using oauth_helper::request
* method (or get() post())
*
* Note:
* 1. This class only support HMAC-SHA1
* 2. oauth_helper class don't store tokens and secrets, you must store them manually
* 3. Some functions are based on http://code.google.com/p/oauth/
*
* @package moodlecore
* @copyright 2010 Dongsheng Cai <dongsheng@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class oauth_helper {
/** @var string consumer key, issued by oauth provider*/
protected $consumer_key;
/** @var string consumer secret, issued by oauth provider*/
protected $consumer_secret;
/** @var string oauth root*/
protected $api_root;
/** @var string request token url*/
protected $request_token_api;
/** @var string authorize url*/
protected $authorize_url;
protected $http_method;
/** @var string */
protected $access_token_api;
/** @var curl */
protected $http;
/** @var array options to pass to the next curl request */
protected $http_options;
/** @var moodle_url oauth callback URL. */
protected $oauth_callback;
/** @var string access token. */
protected $access_token;
/** @var string access secret token. */
protected $access_token_secret;
/** @var string sign secret. */
protected $sign_secret;
/** @var string nonce. */
protected $nonce;
/** @var int timestamp. */
protected $timestamp;
/**
* Contructor for oauth_helper.
* Subclass can override construct to build its own $this->http
*
* @param array $args requires at least three keys, oauth_consumer_key
* oauth_consumer_secret and api_root, oauth_helper will
* guess request_token_api, authrize_url and access_token_api
* based on api_root, but it not always works
*/
function __construct($args) {
if (!empty($args['api_root'])) {
$this->api_root = $args['api_root'];
} else {
$this->api_root = '';
}
$this->consumer_key = $args['oauth_consumer_key'];
$this->consumer_secret = $args['oauth_consumer_secret'];
if (empty($args['request_token_api'])) {
$this->request_token_api = $this->api_root . '/request_token';
} else {
$this->request_token_api = $args['request_token_api'];
}
if (empty($args['authorize_url'])) {
$this->authorize_url = $this->api_root . '/authorize';
} else {
$this->authorize_url = $args['authorize_url'];
}
if (empty($args['access_token_api'])) {
$this->access_token_api = $this->api_root . '/access_token';
} else {
$this->access_token_api = $args['access_token_api'];
}
if (!empty($args['oauth_callback'])) {
$this->oauth_callback = new moodle_url($args['oauth_callback']);
}
if (!empty($args['access_token'])) {
$this->access_token = $args['access_token'];
}
if (!empty($args['access_token_secret'])) {
$this->access_token_secret = $args['access_token_secret'];
}
$this->http = new curl(array('debug'=>false));
if (!empty($args['http_options'])) {
$this->http_options = $args['http_options'];
} else {
$this->http_options = array();
}
}
/**
* Build parameters list:
* oauth_consumer_key="0685bd9184jfhq22",
* oauth_nonce="4572616e48616d6d65724c61686176",
* oauth_token="ad180jjd733klru7",
* oauth_signature_method="HMAC-SHA1",
* oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
* oauth_timestamp="137131200",
* oauth_version="1.0"
* oauth_verifier="1.0"
* @param array $param
* @return string
*/
function get_signable_parameters($params){
$sorted = $params;
ksort($sorted);
$total = array();
foreach ($sorted as $k => $v) {
if ($k == 'oauth_signature') {
continue;
}
$total[] = rawurlencode($k) . '=' . rawurlencode($v);
}
return implode('&', $total);
}
/**
* Create signature for oauth request
* @param string $url
* @param string $secret
* @param array $params
* @return string
*/
public function sign($http_method, $url, $params, $secret) {
$sig = array(
strtoupper($http_method),
preg_replace('/%7E/', '~', rawurlencode($url)),
rawurlencode($this->get_signable_parameters($params)),
);
$base_string = implode('&', $sig);
$sig = base64_encode(hash_hmac('sha1', $base_string, $secret, true));
return $sig;
}
/**
* Initilize oauth request parameters, including:
* oauth_consumer_key="0685bd9184jfhq22",
* oauth_token="ad180jjd733klru7",
* oauth_signature_method="HMAC-SHA1",
* oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
* oauth_timestamp="137131200",
* oauth_nonce="4572616e48616d6d65724c61686176",
* oauth_version="1.0"
* To access protected resources, oauth_token should be defined
*
* @param string $url
* @param string $token
* @param string $http_method
* @return array
*/
public function prepare_oauth_parameters($url, $params, $http_method = 'POST') {
if (is_array($params)) {
$oauth_params = $params;
} else {
$oauth_params = array();
}
$oauth_params['oauth_version'] = '1.0';
$oauth_params['oauth_nonce'] = $this->get_nonce();
$oauth_params['oauth_timestamp'] = $this->get_timestamp();
$oauth_params['oauth_consumer_key'] = $this->consumer_key;
$oauth_params['oauth_signature_method'] = 'HMAC-SHA1';
$oauth_params['oauth_signature'] = $this->sign($http_method, $url, $oauth_params, $this->sign_secret);
return $oauth_params;
}
public function setup_oauth_http_header($params) {
$total = array();
ksort($params);
foreach ($params as $k => $v) {
$total[] = rawurlencode($k) . '="' . rawurlencode($v).'"';
}
$str = implode(', ', $total);
$str = 'Authorization: OAuth '.$str;
$this->http->setHeader('Expect:');
$this->http->setHeader($str);
}
/**
* Sets the options for the next curl request
*
* @param array $options
*/
public function setup_oauth_http_options($options) {
$this->http_options = $options;
}
/**
* Request token for authentication
* This is the first step to use OAuth, it will return oauth_token and oauth_token_secret
* @return array
*/
public function request_token() {
$this->sign_secret = $this->consumer_secret.'&';
if (empty($this->oauth_callback)) {
$params = [];
} else {
$params = ['oauth_callback' => $this->oauth_callback->out(false)];
}
$params = $this->prepare_oauth_parameters($this->request_token_api, $params, 'GET');
$content = $this->http->get($this->request_token_api, $params, $this->http_options);
// Including:
// oauth_token
// oauth_token_secret
$result = $this->parse_result($content);
if (empty($result['oauth_token'])) {
throw new moodle_exception('oauth1requesttoken', 'core_error', '', null, $content);
}
// Build oauth authorize url.
$result['authorize_url'] = $this->authorize_url . '?oauth_token='.$result['oauth_token'];
return $result;
}
/**
* Set oauth access token for oauth request
* @param string $token
* @param string $secret
*/
public function set_access_token($token, $secret) {
$this->access_token = $token;
$this->access_token_secret = $secret;
}
/**
* Request oauth access token from server
* @param string $method
* @param string $url
* @param string $token
* @param string $secret
*/
public function get_access_token($token, $secret, $verifier='') {
$this->sign_secret = $this->consumer_secret.'&'.$secret;
$params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST');
$this->setup_oauth_http_header($params);
// Should never send the callback in this request.
unset($params['oauth_callback']);
$content = $this->http->post($this->access_token_api, $params, $this->http_options);
$keys = $this->parse_result($content);
if (empty($keys['oauth_token']) || empty($keys['oauth_token_secret'])) {
throw new moodle_exception('oauth1accesstoken', 'core_error', '', null, $content);
}
$this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
return $keys;
}
/**
* Request oauth protected resources
* @param string $method
* @param string $url
* @param string $token
* @param string $secret
*/
public function request($method, $url, $params=array(), $token='', $secret='') {
if (empty($token)) {
$token = $this->access_token;
}
if (empty($secret)) {
$secret = $this->access_token_secret;
}
// to access protected resource, sign_secret will alwasy be consumer_secret+token_secret
$this->sign_secret = $this->consumer_secret.'&'.$secret;
if (strtolower($method) === 'post' && !empty($params)) {
$oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) + $params, $method);
} else {
$oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
}
$this->setup_oauth_http_header($oauth_params);
$content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options));
// reset http header and options to prepare for the next request
$this->http->resetHeader();
// return request return value
return $content;
}
/**
* shortcut to start http get request
*/
public function get($url, $params=array(), $token='', $secret='') {
return $this->request('GET', $url, $params, $token, $secret);
}
/**
* shortcut to start http post request
*/
public function post($url, $params=array(), $token='', $secret='') {
return $this->request('POST', $url, $params, $token, $secret);
}
/**
* A method to parse oauth response to get oauth_token and oauth_token_secret
* @param string $str
* @return array
*/
public function parse_result($str) {
if (empty($str)) {
throw new moodle_exception('error');
}
$parts = explode('&', $str);
$result = array();
foreach ($parts as $part){
list($k, $v) = explode('=', $part, 2);
$result[urldecode($k)] = urldecode($v);
}
if (empty($result)) {
throw new moodle_exception('error');
}
return $result;
}
/**
* Set nonce
*/
function set_nonce($str) {
$this->nonce = $str;
}
/**
* Set timestamp
*/
function set_timestamp($time) {
$this->timestamp = $time;
}
/**
* Generate timestamp
*/
function get_timestamp() {
if (!empty($this->timestamp)) {
$timestamp = $this->timestamp;
unset($this->timestamp);
return $timestamp;
}
return time();
}
/**
* Generate nonce for oauth request
*/
function get_nonce() {
if (!empty($this->nonce)) {
$nonce = $this->nonce;
unset($this->nonce);
return $nonce;
}
$mt = microtime();
$rand = mt_rand();
return md5($mt . $rand);
}
}
/**
* OAuth 2.0 Client for using web access tokens.
*
* http://tools.ietf.org/html/draft-ietf-oauth-v2-22
*
* @package core
* @copyright Dan Poltawski <talktodan@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class oauth2_client extends curl {
/** @var string $clientid client identifier issued to the client */
private $clientid = '';
/** @var string $clientsecret The client secret. */
private $clientsecret = '';
/** @var moodle_url $returnurl URL to return to after authenticating */
private $returnurl = null;
/** @var string $scope of the authentication request */
protected $scope = '';
/** @var stdClass $accesstoken access token object */
protected $accesstoken = null;
/** @var string $refreshtoken refresh token string */
protected $refreshtoken = '';
/** @var string $mocknextresponse string */
private $mocknextresponse = '';
/** @var array $upgradedcodes list of upgraded codes in this request */
private static $upgradedcodes = [];
/** @var bool basicauth */
protected $basicauth = false;
/**
* Returns the auth url for OAuth 2.0 request
* @return string the auth url
*/
abstract protected function auth_url();
/**
* Returns the token url for OAuth 2.0 request
* @return string the auth url
*/
abstract protected function token_url();
/**
* Constructor.
*
* @param string $clientid
* @param string $clientsecret
* @param moodle_url $returnurl
* @param string $scope
*/
public function __construct($clientid, $clientsecret, moodle_url $returnurl, $scope) {
parent::__construct();
$this->clientid = $clientid;
$this->clientsecret = $clientsecret;
$this->returnurl = $returnurl;
$this->scope = $scope;
$this->accesstoken = $this->get_stored_token();
}
/**
* Is the user logged in? Note that if this is called
* after the first part of the authorisation flow the token
* is upgraded to an accesstoken.
*
* @return boolean true if logged in
*/
public function is_logged_in() {
// Has the token expired?
if (isset($this->accesstoken->expires) && time() >= $this->accesstoken->expires) {
$this->store_token(null);
return false;
}
// We have a token so we are logged in.
if (isset($this->accesstoken->token)) {
// Check that the access token has all the requested scopes.
$scopemissing = false;
$scopecheck = ' ' . $this->accesstoken->scope . ' ';
$requiredscopes = explode(' ', $this->scope);
foreach ($requiredscopes as $requiredscope) {
if (strpos($scopecheck, ' ' . $requiredscope . ' ') === false) {
$scopemissing = true;
break;
}
}
if (!$scopemissing) {
return true;
}
}
// If we've been passed then authorization code generated by the
// authorization server try and upgrade the token to an access token.
$code = optional_param('oauth2code', null, PARAM_RAW);
// Note - sometimes we may call is_logged_in twice in the same request - we don't want to attempt
// to upgrade the same token twice.
if ($code && !in_array($code, self::$upgradedcodes) && $this->upgrade_token($code)) {
return true;
}
return false;
}
/**
* Callback url where the request is returned to.
*
* @return moodle_url url of callback
*/
public static function callback_url() {
global $CFG;
return new moodle_url('/admin/oauth2callback.php');
}
/**
* An additional array of url params to pass with a login request.
*
* @return array of name value pairs.
*/
public function get_additional_login_parameters() {
return [];
}
/**
* Returns the login link for this oauth request
*
* @return moodle_url login url
*/
public function get_login_url() {
$callbackurl = self::callback_url();
$defaultparams = [
'client_id' => $this->clientid,
'response_type' => 'code',
'redirect_uri' => $callbackurl->out(false),
'state' => $this->returnurl->out_as_local_url(false),
];
if (!empty($this->scope)) {
// The scope should only be included if a value is set.
// If none provided, the server MUST process the request and provide an appropriate documented response.
// See spec https://tools.ietf.org/html/rfc6749#section-3.3
$defaultparams['scope'] = $this->scope;
}
$params = array_merge(
$defaultparams,
$this->get_additional_login_parameters()
);
return new moodle_url($this->auth_url(), $params);
}
/**
* Given an array of name value pairs - build a valid HTTP POST application/x-www-form-urlencoded string.
*
* @param array $params Name / value pairs.
* @return string POST data.
*/
public function build_post_data($params) {
$result = [];
foreach ($params as $name => $value) {
$result[] = urlencode($name) . '=' . urlencode($value);
}
return implode('&', $result);
}
/**
* Upgrade a authorization token from oauth 2.0 to an access token
*
* @param string $code the code returned from the oauth authenticaiton
* @return boolean true if token is upgraded succesfully
*/
public function upgrade_token($code) {
$callbackurl = self::callback_url();
$params = array('code' => $code,
'grant_type' => 'authorization_code',
'redirect_uri' => $callbackurl->out(false),
);
if ($this->basicauth) {
$idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret);
$this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
} else {
$params['client_id'] = $this->clientid;
$params['client_secret'] = $this->clientsecret;
}
// Requests can either use http GET or POST.
if ($this->use_http_get()) {
$response = $this->get($this->token_url(), $params);
} else {
$response = $this->post($this->token_url(), $this->build_post_data($params));
}
if ($this->info['http_code'] !== 200) {
$debuginfo = !empty($this->error) ? $this->error : $response;
throw new moodle_exception('oauth2upgradetokenerror', 'core_error', '', $this->info['http_code'], $debuginfo);
}
$r = json_decode($response);
if (is_null($r)) {
throw new moodle_exception("Could not decode JSON token response");
}
if (!empty($r->error)) {
throw new moodle_exception($r->error . ' ' . $r->error_description);
}
if (!isset($r->access_token)) {
return false;
}
if (isset($r->refresh_token)) {
$this->refreshtoken = $r->refresh_token;
}
// Store the token an expiry time.
$accesstoken = new stdClass;
$accesstoken->token = $r->access_token;
if (isset($r->expires_in)) {
// Expires 10 seconds before actual expiry.
$accesstoken->expires = (time() + ($r->expires_in - 10));
}
$accesstoken->scope = $this->scope;
// Also add the scopes.
self::$upgradedcodes[] = $code;
$this->store_token($accesstoken);
return true;
}
/**
* Logs out of a oauth request, clearing any stored tokens
*/
public function log_out() {
$this->store_token(null);
}
/**
* Make a HTTP request, adding the access token we have
*
* @param string $url The URL to request
* @param array $options
* @param mixed $acceptheader mimetype (as string) or false to skip sending an accept header.
* @return bool
*/
protected function request($url, $options = array(), $acceptheader = 'application/json') {
$murl = new moodle_url($url);
if ($this->accesstoken) {
if ($this->use_http_get()) {
// If using HTTP GET add as a parameter.
$murl->param('access_token', $this->accesstoken->token);
} else {
$this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
}
}
if ($acceptheader) {
$this->setHeader('Accept: ' . $acceptheader);
}
$response = parent::request($murl->out(false), $options);
$this->resetHeader();
return $response;
}
/**
* Multiple HTTP Requests
* This function could run multi-requests in parallel.
*
* @param array $requests An array of files to request
* @param array $options An array of options to set
* @return array An array of results
*/
protected function multi($requests, $options = array()) {
if ($this->accesstoken) {
$this->setHeader('Authorization: Bearer '.$this->accesstoken->token);
}
return parent::multi($requests, $options);
}
/**
* Returns the tokenname for the access_token to be stored
* through multiple requests.
*
* The default implentation is to use the classname combiend
* with the scope.
*
* @return string tokenname for prefernce storage
*/
protected function get_tokenname() {
// This is unusual but should work for most purposes.
return get_class($this).'-'.md5($this->scope);
}
/**
* Store a token between requests. Currently uses
* session named by get_tokenname
*
* @param stdClass|null $token token object to store or null to clear
*/
protected function store_token($token) {
global $SESSION;
$this->accesstoken = $token;
$name = $this->get_tokenname();
if ($token !== null) {
$SESSION->{$name} = $token;
} else {
unset($SESSION->{$name});
}
}
/**
* Get a refresh token!!!
*
* @return string
*/
public function get_refresh_token() {
return $this->refreshtoken;
}
/**
* Retrieve a token stored.
*
* @return stdClass|null token object
*/
protected function get_stored_token() {
global $SESSION;
$name = $this->get_tokenname();
if (isset($SESSION->{$name})) {
return $SESSION->{$name};
}
return null;
}
/**
* Get access token object.
*
* This is just a getter to read the private property.
*
* @return stdClass
*/
public function get_accesstoken() {
return $this->accesstoken;
}
/**
* Get the client ID.
*
* This is just a getter to read the private property.
*
* @return string
*/
public function get_clientid() {
return $this->clientid;
}
/**
* Get the client secret.
*
* This is just a getter to read the private property.
*
* @return string
*/
public function get_clientsecret() {
return $this->clientsecret;
}
/**
* Should HTTP GET be used instead of POST?
* Some APIs do not support POST and want oauth to use
* GET instead (with the auth_token passed as a GET param).
*
* @return bool true if GET should be used
*/
protected function use_http_get() {
return false;
}
}