Skip to content

Commit 88a6385

Browse files
author
Sergey Chernyshev
authored
Merge pull request #288 from sergeychernyshev/master
Implement OAuth2 token refresh
2 parents 4327c3e + 745986e commit 88a6385

2 files changed

Lines changed: 174 additions & 53 deletions

File tree

classes/OAuth2Module.php

Lines changed: 173 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,9 @@ protected function startOAuth2Flow() {
218218
}
219219
}
220220

221-
$login_link = $this->oAuth2LoginLink . '?' . http_build_query($params);
221+
$first_separator = strpos($this->oAuth2LoginLink, '?') === FALSE ? '?' : '&';
222+
223+
$login_link = $this->oAuth2LoginLink . $first_separator . http_build_query($params);
222224

223225
// redirect to the authorization page, they will redirect back
224226
header('Location: ' . $login_link);
@@ -436,7 +438,8 @@ public function getUserByOAuth2Identity($identity, $oauth2_client_id) {
436438
/**
437439
* Retrieves OAuth2 access token from the service and creates new client entry
438440
*
439-
* @param string $code OAuth2 code
441+
* @param string $code OAuth2 code
442+
* @return int Internal OAuth2 client id
440443
*/
441444
public function getOAuth2ClientIDByCode($code) {
442445
// STEP 2: Get access token
@@ -449,6 +452,34 @@ public function getOAuth2ClientIDByCode($code) {
449452
'client_secret' => $this->oAuth2ClientSecret
450453
);
451454

455+
return $this->updateOAuth2Tokens($params);
456+
}
457+
458+
/**
459+
* Refreshes access token for existing credentials
460+
*
461+
* @param OAuth2UserCredentials $credentials Existing user credentials
462+
*/
463+
public function refreshAccessToken($credentials) {
464+
$params = array(
465+
'grant_type' => 'refresh_token',
466+
'refresh_token' => $credentials->getRefreshToken(),
467+
'client_id' => $this->oAuth2ClientID,
468+
'client_secret' => $this->oAuth2ClientSecret
469+
);
470+
471+
UserTools::debug("Refreshing OAuth2 token");
472+
473+
$this->updateOAuth2Tokens($params, $credentials);
474+
}
475+
476+
/**
477+
* Creates or updates OAuth2 tokens and database records
478+
* @param array $params Authorization request parameters
479+
* @param OAuth2UserCredentials|null $current_credentials Current user credentials
480+
* @return int Internal OAuth2 client id
481+
*/
482+
private function updateOAuth2Tokens($params, $current_credentials = null) {
452483
$ch = curl_init();
453484
curl_setopt($ch, CURLOPT_URL, $this->oAuth2AccessTokenURL);
454485
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
@@ -488,10 +519,16 @@ public function getOAuth2ClientIDByCode($code) {
488519
throw new OAuth2Exception("OAuth2 access token is not returned");
489520
}
490521

522+
UserTools::debug("Result: " . var_export($result, TRUE));
523+
524+
UserTools::debug("Access token: " . $access_token);
525+
491526
$refresh_token = array_key_exists('refresh_token', $result) ? $result['refresh_token'] : null;
492527
$expires_in = array_key_exists('expires_in', $result) ? $result['expires_in'] : null;
493528
$token_type = array_key_exists('token_type', $result) ? $result['token_type'] : 'bearer';
494529

530+
UserTools::debug("Refresh token: " . $refresh_token);
531+
495532
UserTools::debug("Token type: $token_type");
496533

497534
if (strtolower($token_type) != 'bearer') {
@@ -505,43 +542,57 @@ public function getOAuth2ClientIDByCode($code) {
505542

506543
$access_token_expires = is_null($expires_in) ? null : time() + $expires_in;
507544

508-
$oauth2_client_id = null;
545+
$oauth2_client_id = $current_credentials ? $current_credentials->getClientId() : null;
509546
$current_expires = null;
510547
$current_refresh = null;
511548

512-
$query = 'SELECT oauth2_client_id, UNIX_TIMESTAMP(access_token_expires), refresh_token
513-
FROM u_oauth2_clients
514-
WHERE module_slug = ? AND access_token = ?';
515-
UserTools::debug($query);
549+
if (!$current_credentials) {
550+
/**
551+
* Let's try reading client info for this access_token from the database
552+
*/
553+
$query = 'SELECT oauth2_client_id, UNIX_TIMESTAMP(access_token_expires), refresh_token
554+
FROM u_oauth2_clients
555+
WHERE module_slug = ? AND access_token = ?';
556+
UserTools::debug($query);
516557

517-
if ($stmt = $db->prepare($query))
518-
{
519-
if (!$stmt->bind_param('ss', $module_slug, $access_token))
520-
{
521-
throw new DBBindParamException($db, $stmt);
522-
}
523-
if (!$stmt->execute())
558+
if ($stmt = $db->prepare($query))
524559
{
525-
throw new DBExecuteStmtException($db, $stmt);
560+
if (!$stmt->bind_param('ss', $module_slug, $access_token))
561+
{
562+
throw new DBBindParamException($db, $stmt);
563+
}
564+
if (!$stmt->execute())
565+
{
566+
throw new DBExecuteStmtException($db, $stmt);
567+
}
568+
if (!$stmt->bind_result($oauth2_client_id, $current_expires, $current_refresh))
569+
{
570+
throw new DBBindResultException($db, $stmt);
571+
}
572+
573+
$stmt->fetch();
574+
$stmt->close();
526575
}
527-
if (!$stmt->bind_result($oauth2_client_id, $current_expires, $current_refresh))
576+
else
528577
{
529-
throw new DBBindResultException($db, $stmt);
578+
throw new DBPrepareStmtException($db);
530579
}
531-
532-
$stmt->fetch();
533-
$stmt->close();
534-
}
535-
else
536-
{
537-
throw new DBPrepareStmtException($db);
538580
}
539581

582+
/**
583+
* If we don't have client_id (for new access_token), let's insert a new one into the database (without identity)
584+
*/
540585
if (!$oauth2_client_id) {
541586
$query = 'INSERT INTO u_oauth2_clients
542587
(module_slug, access_token, access_token_expires, refresh_token)
543588
VALUES (?, ?, FROM_UNIXTIME(?), ?)';
544589
UserTools::debug($query);
590+
UserTools::debug(var_export([
591+
"module_slug" => $module_slug,
592+
"access_token" => $access_token,
593+
"access_token_expires" => $access_token_expires,
594+
"refresh_token" => $refresh_token
595+
], true));
545596

546597
if ($stmt = $db->prepare($query))
547598
{
@@ -563,30 +614,65 @@ public function getOAuth2ClientIDByCode($code) {
563614
} else {
564615
throw new DBPrepareStmtException($db);
565616
}
566-
} else if ($access_token_expires != $current_expires
617+
} else {
618+
/**
619+
* Otherwise update the token if we a refreshing the token or expires timestamp / refresh token are updated
620+
*/
621+
if ($current_credentials
622+
|| $access_token_expires != $current_expires
567623
|| $refresh_token != $current_refresh) {
568-
$query = 'UPDATE u_oauth2_clients
569-
SET access_token_expires = FROM_UNIXTIME(?), refresh_token = ?
570-
WHERE oauth2_client_id = ?';
571-
UserTools::debug($query);
572624

573-
if ($stmt = $db->prepare($query))
574-
{
575-
if (!$stmt->bind_param('ssi',
576-
$access_token_expires,
577-
$refresh_token,
578-
$oauth2_client_id))
579-
{
580-
throw new DBBindParamException($db, $stmt);
625+
// if refresh token is in fact passed, update it, otherwise keep the old one
626+
if ($refresh_token) {
627+
$query = 'UPDATE u_oauth2_clients
628+
SET access_token = ?, access_token_expires = FROM_UNIXTIME(?), refresh_token = ?
629+
WHERE oauth2_client_id = ?';
630+
} else {
631+
$query = 'UPDATE u_oauth2_clients
632+
SET access_token = ?, access_token_expires = FROM_UNIXTIME(?)
633+
WHERE oauth2_client_id = ?';
581634
}
582-
if (!$stmt->execute())
635+
636+
UserTools::debug($query);
637+
UserTools::debug(var_export([
638+
"module_slug" => $module_slug,
639+
"access_token" => $access_token,
640+
"access_token_expires" => $access_token_expires,
641+
"refresh_token" => $refresh_token,
642+
"oauth2_client_id" => $oauth2_client_id
643+
], true));
644+
645+
if ($stmt = $db->prepare($query))
583646
{
584-
throw new DBExecuteStmtException($db, $stmt);
647+
if ($refresh_token) {
648+
if (!$stmt->bind_param('sssi',
649+
$access_token,
650+
$access_token_expires,
651+
$refresh_token,
652+
$oauth2_client_id))
653+
{
654+
throw new DBBindParamException($db, $stmt);
655+
}
656+
} else {
657+
if (!$stmt->bind_param('ssi',
658+
$access_token,
659+
$access_token_expires,
660+
$oauth2_client_id))
661+
{
662+
throw new DBBindParamException($db, $stmt);
663+
}
664+
}
665+
if (!$stmt->execute())
666+
{
667+
throw new DBExecuteStmtException($db, $stmt);
668+
}
669+
670+
$stmt->close();
671+
} else {
672+
throw new DBPrepareStmtException($db);
585673
}
586674

587-
$stmt->close();
588-
} else {
589-
throw new DBPrepareStmtException($db);
675+
$current_credentials->updateCredentials($access_token, $access_token_expires, $refresh_token);
590676
}
591677
}
592678

@@ -1003,19 +1089,18 @@ public function getTotalConnectedUsers()
10031089
* This method allows requesting information on behalf of the user from a 3rd party provider.
10041090
* Possibly the most important feature of the whole system.
10051091
*
1006-
* @param User $user User to make request for
1092+
* @param UserCredentials $credentials Credentials object representing OAuth2 user
10071093
* @param string $request Request URL
10081094
* @param string $method HTTP method (e.g. GET, POST, PUT, etc)
10091095
* @param array $params Request parameters key->value array
1096+
* @param boolean $refresh_if_unauthorized Whatever to attempt to refresh the token on failure (to avoid recursion)
10101097
*
10111098
* @return array Response data (code=>int, headers=>array(), body=>string)
10121099
*/
1013-
public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_params = array(), $curlopt = array())
1100+
public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_params = array(), $curlopt = array(), $refresh_if_unauthorized = TRUE)
10141101
{
10151102
$ch = curl_init();
10161103

1017-
$separator = strpos($url, '?') ? '&' : '?';
1018-
10191104
if (!is_array($request_params)) {
10201105
$request_params = array();
10211106
}
@@ -1024,11 +1109,12 @@ public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_
10241109
}
10251110
$params = array_merge($request_params, $this->oAuth2ExtraParameters);
10261111

1112+
$first_separator = strpos($url, '?') === FALSE ? '?' : '&';
1113+
10271114
// always pass access_token as a query string parameter
10281115
if (count($params)) {
10291116
if ($method == 'GET') {
1030-
$url .= $separator . http_build_query($params);
1031-
$separator = '&';
1117+
$call_url = $url . $first_separator . http_build_query($params);
10321118
} else if ($method == 'POST') {
10331119
$curlopt[CURLOPT_POST] = TRUE;
10341120
$curlopt[CURLOPT_POSTFIELDS] = $params;
@@ -1039,15 +1125,14 @@ public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_
10391125
if ($this->oAuth2SendAccessTokenAsHeader) {
10401126
$curlopt[CURLOPT_HTTPHEADER][] = 'Authorization: Bearer ' . $credentials->getAccessToken();
10411127
} else {
1042-
$url .= $separator . http_build_query(array(
1128+
$call_url = $url . $first_separator . http_build_query(array(
10431129
$this->oAuth2AccessTokenParamName => $credentials->getAccessToken()
10441130
));
1045-
$separator = '&';
10461131
}
10471132

1048-
UserTools::debug("URL: $url");
1133+
UserTools::debug("URL: $call_url");
10491134

1050-
curl_setopt($ch, CURLOPT_URL, $url);
1135+
curl_setopt($ch, CURLOPT_URL, $call_url);
10511136
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
10521137
curl_setopt($ch, CURLOPT_HEADER, FALSE);
10531138
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
@@ -1060,6 +1145,21 @@ public function makeOAuth2Request($credentials, $url, $method = 'GET', $request_
10601145
$result = curl_exec($ch);
10611146
UserTools::debug("Request: " . var_export(curl_getinfo($ch, CURLINFO_HEADER_OUT), true));
10621147
UserTools::debug("Response: $result");
1148+
UserTools::debug("HTTP Response code: " . curl_getinfo($ch, CURLINFO_HTTP_CODE));
1149+
1150+
// let's see if our access token has expired and try to refresh it
1151+
if ($result) {
1152+
$data = json_decode($result, true);
1153+
1154+
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 401 || array_key_exists('code', $data) && $data['code'] == 'not_authorized') {
1155+
$this->refreshAccessToken($credentials);
1156+
1157+
// call thyself without refreshing on failure (to avoid recursion)
1158+
1159+
// @TODO debug why this goes into infinite loop before re-emabling it
1160+
return $this->makeOAuth2Request($credentials, $url, $method, $request_params, $curlopt, FALSE);
1161+
}
1162+
}
10631163

10641164
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) {
10651165
throw new OAuth2Exception("OAuth2 call failed: " . curl_error($ch) . ' (Code: ' . curl_getinfo($ch, CURLINFO_HTTP_CODE) . ')');
@@ -1166,6 +1266,27 @@ public function getAccessToken() {
11661266
return $this->access_token;
11671267
}
11681268

1269+
/**
1270+
* Returns OAuth2 refresh token
1271+
*
1272+
* @return string OAuth2 refresh token
1273+
*/
1274+
public function getRefreshToken() {
1275+
return $this->refresh_token;
1276+
}
1277+
1278+
public function getClientId() {
1279+
return $this->oauth2_client_id;
1280+
}
1281+
1282+
public function updateCredentials($access_token, $access_token_expires, $refresh_token) {
1283+
$this->access_token = $access_token;
1284+
$this->access_token_expires = $access_token_expires;
1285+
if ($refresh_token) {
1286+
$this->refresh_token = $refresh_token;
1287+
}
1288+
}
1289+
11691290
public function makeOAuth2Request($request, $method = 'GET', $params = null, $curlopt = array()) {
11701291
return $this->oauth2_module->makeOAuth2Request($this, $request, $method, $params, $curlopt);
11711292
}

modules/google/index.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function __construct($oAuth2ClientID, $oAuth2ClientSecret,
2020
'https://www.googleapis.com/',
2121
$oAuth2ClientID,
2222
$oAuth2ClientSecret,
23-
'https://accounts.google.com/o/oauth2/auth',
23+
'https://accounts.google.com/o/oauth2/auth?access_type=offline',
2424
'https://www.googleapis.com/oauth2/v4/token',
2525
$scopes,
2626
UserConfig::$USERSROOTURL.'/modules/google/signup-button.png',

0 commit comments

Comments
 (0)