Permalink
Browse files

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/.
  • Loading branch information...
danpoltawski committed May 13, 2012
1 parent f40f6c1 commit 469fb5d6722678c68b3e8711b08d1271d3d8c411
Showing with 299 additions and 1 deletion.
  1. +38 −0 admin/oauth2callback.php
  2. +261 −1 lib/oauthlib.php
View
@@ -0,0 +1,38 @@
<?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/>.
/**
* 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)));
View
@@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@@ -15,6 +14,11 @@
// 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
*
@@ -349,3 +353,259 @@ function get_nonce() {
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 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;
}
}

0 comments on commit 469fb5d

Please sign in to comment.