+
+/**
+ * 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 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;
+ }
+}