Permalink
Browse files

Merge pull request #288 from sergeychernyshev/master

Implement OAuth2 token refresh
  • Loading branch information...
sergeychernyshev committed Feb 18, 2018
2 parents 4327c3e + 745986e commit 88a6385d3347c72ef8525276378925cbc936510d
Showing with 174 additions and 53 deletions.
  1. +173 −52 classes/OAuth2Module.php
  2. +1 −1 modules/google/index.php
View
@@ -218,7 +218,9 @@ protected function startOAuth2Flow() {
}
}
$login_link = $this->oAuth2LoginLink . '?' . http_build_query($params);
$first_separator = strpos($this->oAuth2LoginLink, '?') === FALSE ? '?' : '&';
$login_link = $this->oAuth2LoginLink . $first_separator . http_build_query($params);
// redirect to the authorization page, they will redirect back
header('Location: ' . $login_link);
@@ -436,7 +438,8 @@ public function getUserByOAuth2Identity($identity, $oauth2_client_id) {
/**
* Retrieves OAuth2 access token from the service and creates new client entry
*
* @param string $code OAuth2 code
* @param string $code OAuth2 code
* @return int Internal OAuth2 client id
*/
public function getOAuth2ClientIDByCode($code) {
// STEP 2: Get access token
@@ -449,6 +452,34 @@ public function getOAuth2ClientIDByCode($code) {
'client_secret' => $this->oAuth2ClientSecret
);
return $this->updateOAuth2Tokens($params);
}
/**
* Refreshes access token for existing credentials
*
* @param OAuth2UserCredentials $credentials Existing user credentials
*/
public function refreshAccessToken($credentials) {
$params = array(
'grant_type' => 'refresh_token',
'refresh_token' => $credentials->getRefreshToken(),
'client_id' => $this->oAuth2ClientID,
'client_secret' => $this->oAuth2ClientSecret
);
UserTools::debug("Refreshing OAuth2 token");
$this->updateOAuth2Tokens($params, $credentials);
}
/**
* Creates or updates OAuth2 tokens and database records
* @param array $params Authorization request parameters
* @param OAuth2UserCredentials|null $current_credentials Current user credentials
* @return int Internal OAuth2 client id
*/
private function updateOAuth2Tokens($params, $current_credentials = null) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->oAuth2AccessTokenURL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
@@ -488,10 +519,16 @@ public function getOAuth2ClientIDByCode($code) {
throw new OAuth2Exception("OAuth2 access token is not returned");
}
UserTools::debug("Result: " . var_export($result, TRUE));
UserTools::debug("Access token: " . $access_token);
$refresh_token = array_key_exists('refresh_token', $result) ? $result['refresh_token'] : null;
$expires_in = array_key_exists('expires_in', $result) ? $result['expires_in'] : null;
$token_type = array_key_exists('token_type', $result) ? $result['token_type'] : 'bearer';
UserTools::debug("Refresh token: " . $refresh_token);
UserTools::debug("Token type: $token_type");
if (strtolower($token_type) != 'bearer') {
@@ -505,43 +542,57 @@ public function getOAuth2ClientIDByCode($code) {
$access_token_expires = is_null($expires_in) ? null : time() + $expires_in;
$oauth2_client_id = null;
$oauth2_client_id = $current_credentials ? $current_credentials->getClientId() : null;
$current_expires = null;
$current_refresh = null;
$query = 'SELECT oauth2_client_id, UNIX_TIMESTAMP(access_token_expires), refresh_token
FROM u_oauth2_clients
WHERE module_slug = ? AND access_token = ?';
UserTools::debug($query);
if (!$current_credentials) {
/**
* Let's try reading client info for this access_token from the database
*/
$query = 'SELECT oauth2_client_id, UNIX_TIMESTAMP(access_token_expires), refresh_token
FROM u_oauth2_clients
WHERE module_slug = ? AND access_token = ?';
UserTools::debug($query);
if ($stmt = $db->prepare($query))
{
if (!$stmt->bind_param('ss', $module_slug, $access_token))
{
throw new DBBindParamException($db, $stmt);
}
if (!$stmt->execute())
if ($stmt = $db->prepare($query))
{
throw new DBExecuteStmtException($db, $stmt);
if (!$stmt->bind_param('ss', $module_slug, $access_token))
{
throw new DBBindParamException($db, $stmt);
}
if (!$stmt->execute())
{
throw new DBExecuteStmtException($db, $stmt);
}
if (!$stmt->bind_result($oauth2_client_id, $current_expires, $current_refresh))
{
throw new DBBindResultException($db, $stmt);
}
$stmt->fetch();
$stmt->close();
}
if (!$stmt->bind_result($oauth2_client_id, $current_expires, $current_refresh))
else
{
throw new DBBindResultException($db, $stmt);
throw new DBPrepareStmtException($db);
}
$stmt->fetch();
$stmt->close();
}
else
{
throw new DBPrepareStmtException($db);
}
/**
* If we don't have client_id (for new access_token), let's insert a new one into the database (without identity)
*/
if (!$oauth2_client_id) {
$query = 'INSERT INTO u_oauth2_clients
(module_slug, access_token, access_token_expires, refresh_token)
VALUES (?, ?, FROM_UNIXTIME(?), ?)';
UserTools::debug($query);
UserTools::debug(var_export([
"module_slug" => $module_slug,
"access_token" => $access_token,
"access_token_expires" => $access_token_expires,
"refresh_token" => $refresh_token
], true));
if ($stmt = $db->prepare($query))
{
@@ -563,30 +614,65 @@ public function getOAuth2ClientIDByCode($code) {
} else {
throw new DBPrepareStmtException($db);
}
} else if ($access_token_expires != $current_expires
} else {
/**
* Otherwise update the token if we a refreshing the token or expires timestamp / refresh token are updated
*/
if ($current_credentials
|| $access_token_expires != $current_expires
|| $refresh_token != $current_refresh) {
$query = 'UPDATE u_oauth2_clients
SET access_token_expires = FROM_UNIXTIME(?), refresh_token = ?
WHERE oauth2_client_id = ?';
UserTools::debug($query);
if ($stmt = $db->prepare($query))
{
if (!$stmt->bind_param('ssi',
$access_token_expires,
$refresh_token,
$oauth2_client_id))
{
throw new DBBindParamException($db, $stmt);
// if refresh token is in fact passed, update it, otherwise keep the old one
if ($refresh_token) {
$query = 'UPDATE u_oauth2_clients
SET access_token = ?, access_token_expires = FROM_UNIXTIME(?), refresh_token = ?
WHERE oauth2_client_id = ?';
} else {
$query = 'UPDATE u_oauth2_clients
SET access_token = ?, access_token_expires = FROM_UNIXTIME(?)
WHERE oauth2_client_id = ?';
}
if (!$stmt->execute())
UserTools::debug($query);
UserTools::debug(var_export([
"module_slug" => $module_slug,
"access_token" => $access_token,
"access_token_expires" => $access_token_expires,
"refresh_token" => $refresh_token,
"oauth2_client_id" => $oauth2_client_id
], true));
if ($stmt = $db->prepare($query))
{
throw new DBExecuteStmtException($db, $stmt);
if ($refresh_token) {
if (!$stmt->bind_param('sssi',
$access_token,
$access_token_expires,
$refresh_token,
$oauth2_client_id))
{
throw new DBBindParamException($db, $stmt);
}
} else {
if (!$stmt->bind_param('ssi',
$access_token,
$access_token_expires,
$oauth2_client_id))
{
throw new DBBindParamException($db, $stmt);
}
}
if (!$stmt->execute())
{
throw new DBExecuteStmtException($db, $stmt);
}
$stmt->close();
} else {
throw new DBPrepareStmtException($db);
}
$stmt->close();
} else {
throw new DBPrepareStmtException($db);
$current_credentials->updateCredentials($access_token, $access_token_expires, $refresh_token);
}
}
@@ -1003,19 +1089,18 @@ public function getTotalConnectedUsers()
* This method allows requesting information on behalf of the user from a 3rd party provider.
* Possibly the most important feature of the whole system.
*
* @param User $user User to make request for
* @param UserCredentials $credentials Credentials object representing OAuth2 user
* @param string $request Request URL
* @param string $method HTTP method (e.g. GET, POST, PUT, etc)
* @param array $params Request parameters key->value array
* @param boolean $refresh_if_unauthorized Whatever to attempt to refresh the token on failure (to avoid recursion)
*
* @return array Response data (code=>int, headers=>array(), body=>string)
*/
public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_params = array(), $curlopt = array())
public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_params = array(), $curlopt = array(), $refresh_if_unauthorized = TRUE)
{
$ch = curl_init();
$separator = strpos($url, '?') ? '&' : '?';
if (!is_array($request_params)) {
$request_params = array();
}
@@ -1024,11 +1109,12 @@ public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_
}
$params = array_merge($request_params, $this->oAuth2ExtraParameters);
$first_separator = strpos($url, '?') === FALSE ? '?' : '&';
// always pass access_token as a query string parameter
if (count($params)) {
if ($method == 'GET') {
$url .= $separator . http_build_query($params);
$separator = '&';
$call_url = $url . $first_separator . http_build_query($params);
} else if ($method == 'POST') {
$curlopt[CURLOPT_POST] = TRUE;
$curlopt[CURLOPT_POSTFIELDS] = $params;
@@ -1039,15 +1125,14 @@ public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_
if ($this->oAuth2SendAccessTokenAsHeader) {
$curlopt[CURLOPT_HTTPHEADER][] = 'Authorization: Bearer ' . $credentials->getAccessToken();
} else {
$url .= $separator . http_build_query(array(
$call_url = $url . $first_separator . http_build_query(array(
$this->oAuth2AccessTokenParamName => $credentials->getAccessToken()
));
$separator = '&';
}
UserTools::debug("URL: $url");
UserTools::debug("URL: $call_url");
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_URL, $call_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
@@ -1060,6 +1145,21 @@ public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_
$result = curl_exec($ch);
UserTools::debug("Request: " . var_export(curl_getinfo($ch, CURLINFO_HEADER_OUT), true));
UserTools::debug("Response: $result");
UserTools::debug("HTTP Response code: " . curl_getinfo($ch, CURLINFO_HTTP_CODE));
// let's see if our access token has expired and try to refresh it
if ($result) {
$data = json_decode($result, true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 401 || array_key_exists('code', $data) && $data['code'] == 'not_authorized') {
$this->refreshAccessToken($credentials);
// call thyself without refreshing on failure (to avoid recursion)
// @TODO debug why this goes into infinite loop before re-emabling it
return $this->makeOAuth2Request($credentials, $url, $method, $request_params, $curlopt, FALSE);
}
}
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) {
throw new OAuth2Exception("OAuth2 call failed: " . curl_error($ch) . ' (Code: ' . curl_getinfo($ch, CURLINFO_HTTP_CODE) . ')');
@@ -1166,6 +1266,27 @@ public function getAccessToken() {
return $this->access_token;
}
/**
* Returns OAuth2 refresh token
*
* @return string OAuth2 refresh token
*/
public function getRefreshToken() {
return $this->refresh_token;
}
public function getClientId() {
return $this->oauth2_client_id;
}
public function updateCredentials($access_token, $access_token_expires, $refresh_token) {
$this->access_token = $access_token;
$this->access_token_expires = $access_token_expires;
if ($refresh_token) {
$this->refresh_token = $refresh_token;
}
}
public function makeOAuth2Request($request, $method = 'GET', $params = null, $curlopt = array()) {
return $this->oauth2_module->makeOAuth2Request($this, $request, $method, $params, $curlopt);
}
View
@@ -20,7 +20,7 @@ public function __construct($oAuth2ClientID, $oAuth2ClientSecret,
'https://www.googleapis.com/',
$oAuth2ClientID,
$oAuth2ClientSecret,
'https://accounts.google.com/o/oauth2/auth',
'https://accounts.google.com/o/oauth2/auth?access_type=offline',
'https://www.googleapis.com/oauth2/v4/token',
$scopes,
UserConfig::$USERSROOTURL.'/modules/google/signup-button.png',

0 comments on commit 88a6385

Please sign in to comment.