From 469fb5d6722678c68b3e8711b08d1271d3d8c411 Mon Sep 17 00:00:00 2001 From: Dan Poltawski Date: Mon, 14 May 2012 00:39:00 +0800 Subject: [PATCH] MDL-29857 - lib: Add an OAuth 2.0 client A generic OAuth 2.0 for the web application flow, tested against microsoft and google apis. Added a callback endpoint for requests so that clients can use a single endpoint (without GET params). I put this in /admin/ as I expect some sites will have .htaccess denying access to /lib/. --- admin/oauth2callback.php | 38 ++++++ lib/oauthlib.php | 262 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 admin/oauth2callback.php diff --git a/admin/oauth2callback.php b/admin/oauth2callback.php new file mode 100644 index 00000000000..364c0023de0 --- /dev/null +++ b/admin/oauth2callback.php @@ -0,0 +1,38 @@ +. + +/** + * An oauth2 redirection endpoint which can be used for an application: + * http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-3.1.2 + * + * This is used because some oauth servers will not allow a redirect urls + * with get params (like repository callback) and that needs to be called + * using the state param. + * + * @package core + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(dirname(__FILE__)).'/config.php'); + +// The authorization code generated by the authorization server. +$code = required_param('code', PARAM_RAW); +// The state parameter we've given (used in moodle as a redirect url). +$state = required_param('state', PARAM_URL); + +redirect(new moodle_url($state, array('code' => $code))); diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 36ee14e9f9b..b4aa15b7500 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -1,5 +1,4 @@ . + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/filelib.php'); + /** * OAuth helper class * @@ -349,3 +353,259 @@ class oauth_helper { 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 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class oauth2_client extends curl { + /** var string client identifier issued to the client */ + private $clientid = ''; + /** var string The client secret. */ + private $clientsecret = ''; + /** var string URL to return to after authenticating */ + private $returnurl = ''; + /** var string scope of the authentication request */ + private $scope = ''; + /** var stdClass access token object */ + private $accesstoken = null; + + /** + * 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 string $returnurl + * @param string $scope + */ + public function __construct($clientid, $clientsecret, $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->log_out(); + return false; + } + + // We have a token so we are logged in. + if (isset($this->accesstoken->token)) { + 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('code', null, PARAM_RAW); + if ($code && $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'); + } + + /** + * Returns the login link for this oauth request + * + * @return moodle_url login url + */ + public function get_login_url() { + + $callbackurl = self::callback_url(); + $url = new moodle_url($this->auth_url(), + array('client_id' => $this->clientid, + 'response_type' => 'code', + 'redirect_uri' => $callbackurl->out(false), + 'state' => $this->returnurl, + 'scope' => $this->scope, + )); + + return $url; + } + + /** + * 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('client_id' => $this->clientid, + 'client_secret' => $this->clientsecret, + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $callbackurl->out(false), + ); + + // 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(), $params); + } + + if (!$this->info['http_code'] === 200) { + throw new moodle_exception('Could not upgrade oauth token'); + } + + $r = json_decode($response); + + if (!isset($r->access_token)) { + return false; + } + + // Store the token an expiry time. + $accesstoken = new stdClass; + $accesstoken->token = $r->access_token; + $accesstoken->expires = (time() + ($r->expires_in - 10)); // Expires 10 seconds before actual expiry. + $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 + * @return bool + */ + protected function request($url, $options = array()) { + $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); + } + } + + return parent::request($murl->out(false), $options); + } + + /** + * 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}); + } + } + + /** + * 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; + } + + /** + * 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; + } +} -- 2.43.0