I'm trying to create a (somewhat) seamless app in PHP using Microsoft's Graph API. The application is to be run via command-line, but for initial authorization, instructs the user to open the authorization URL in their browser. The redirect URI is the same script, so when Microsoft redirects back with the code, I capture it and save it to a file on the server.
The user then goes back to the command line and runs the app once again. At this point, the application retrieves all of the user's email data (headers and message data) for processing.
This all works smoothly up until the point that my token expires. I end up having to delete the token file and start the process over. Not convenient while debugging, and definitely not desired in production. Ideally, I'd like to simply refresh the token behind the scenes and have the processing continue without interruption.
My script:
$rootPath = dirname(__DIR__);
require $rootPath.'/common.php';
require $rootPath.'/lib/microsoft.php';
$ms = new Azure();
// check if there is an existing token file, and use that token if there is.
$tokenPath = $rootPath.'/assets/files/tokens/ms.json';
if(file_exists($tokenPath)){
$string = file_get_contents($tokenPath);
$token = json_decode($string, true);
process($token);
}else{
// there is no existing token file, so we must obtain, or be obtaining a new one.
if(isset($_GET['code'])){
// obtaining a new token from Microsoft
$accessToken = $ms->obtainToken($_GET['code']);
$token = $accessToken->jsonSerialize();
// save token to file
saveFile($tokenPath, json_encode($token));
process($token);
}else{
// initial run. we must first login and redirect back to obtain an auth code
$authUrl = $ms->getClient()->getAuthorizationUrl();
printf("Open the following link in your browser:
%s
", $authUrl);
}
}
function process($token){
global $ms;
// obtain a graph instance with the token we have
$graph = $ms->getGraph($token);
$messages = $ms->getMessages();
foreach($messages as $msg){
$headers = $msg['headers'];
print_r($headers);
$message = $msg['message'];
print_r($message);
}
}
microsoft.php
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model;
class Azure {
public $tokenPath;
public $client;
public $graph;
public $token;
public function __construct(){
global $rootPath, $config;
$this->tokenPath = $rootPath.'assets/files/tokens/ms.json';
$this->client = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => $config['vendor']['microsoft']['client'],
'clientSecret' => $config['vendor']['microsoft']['secret'],
'redirectUri' => $config['vendor']['microsoft']['redirect_uri'],
'urlAuthorize' => $config['vendor']['microsoft']['authority'].$config['vendor']['microsoft']['auth_endpoint'],
'urlAccessToken' => $config['vendor']['microsoft']['authority'].$config['vendor']['microsoft']['token_endpoint'],
'urlResourceOwnerDetails' => '',
'scopes' => $config['vendor']['microsoft']['scopes']
]);
$this->graph = new Graph();
}
public function getClient(){
return $this->client;
}
public function getGraph($token){
try{
$this->graph->setAccessToken($token);
return $this->graph;
}catch(Exception $ex){
echo $ex->getMessage();
}
}
public function obtainToken($accessCode){
try {
$this->token = $this->client->getAccessToken('authorization_code', [
'code' => $accessCode
]);
return $this->token;
}catch (League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
exit('ERROR getting tokens: '.$e->getMessage());
}
}
public function refreshToken(){
global $rootPath;
$string = file_get_contents($this->tokenPath);
$token = json_decode($string, true);
$newToken = $this->client->getAccessToken('refresh_token', [
'refresh_token' => $token['refresh_token']
]);
$this->token = $newToken;
$tokenData = $this->token->jsonSerialize();
unlink($this->tokenPath);
saveFile($this->tokenPath, json_encode($tokenData));
return $this->token;
}
public function getToken(){
if($this->token !== null){
$now = time() + 300;
if($this->token->getExpires() <= $now){
$this->refreshToken();
}
return $this->token;
}else{
return $this->refreshToken();
}
}
public function getMessages(){
try{
$params = array(
"\$select" => "id",
"\$filter" => "isRead ne true",
"\$count" => "true"
);
$getMessagesUrl = '/me/mailfolders/inbox/messages?'.http_build_query($params);
$messageList = $this->graph->createRequest('GET', $getMessagesUrl)
->setReturnType(Model\Message::class)
->execute();
unset($params);
$messages = [];
foreach($messageList as $msg){
$msgId = $msg->getId();
$params = array(
"\$select" => "internetMessageHeaders"
);
$getMessageHeadersUrl = '/me/messages/'.$msgId.'/?'.http_build_query($params);
$headerObj = $this->graph->createRequest('GET', $getMessageHeadersUrl)
->setReturnType(Model\Message::class)
->execute();
$headers = [];
$internetMessageHeaders = $headerObj->getInternetMessageHeaders();
foreach($internetMessageHeaders as $header){
$headers[$header['name']] = $header['value'];
}
$params = array(
"\$select" => "*"
);
$getMessageUrl = '/me/messages/'.$msgId.'/';
$message = $this->graph->createRequest('GET', $getMessageUrl)
->setReturnType(Model\Message::class)
->execute();
$messages[] = array(
'headers' => $headers,
'message' => $message
);
}
return $messages;
}catch(Exception $ex){
if(stristr($ex->getMessage(), 'Access token has expired.')){
$this->refreshToken();
}
}
}
}
I don't understand what I'm doing wrong. How can I refresh the token without interrupting the workflow?