Automatically refreshing OAuth access tokens with PHP

A simple system that automatically refreshes an expired OAuth access token if a call is attempted but the token is expired.

This post is a continuation of Twitch authentication with OAuth using PHP.

Method

The two main methods would be to check the current date-time against the expiration date time or this method which upon a HTTP error 401 code the token is refreshed. The first method is ideal however this example is based around using Twitch’s API which doesn’t give a token expiry time when refreshing.

The error 401 code method works as 401 error code means unauthorized, therefore your access token is not valid. It’s important to note that you must have calls working before you apply this method to ensure the unauthorized error is because of the expired token.

Storing the access and refresh token in a plain text file is okay as if someone was to find this file, they can not do anything with either of the tokens. The tokens are only useful when they’re paired with the client id key which is hardcoded and not exposed.

Process

The process is to do an API call however check if a 401 error exists, if it does then refresh the token and redo the call.

Ensuring that a check is done for the token file existing if it isn’t then it will; be created and filled with the access and refresh token.

main functions

Class constants:

const CLIENT_ID = 'XXXX';
const CLIENT_SECRET = 'XXXX';
const ACCESS_CODE = 'XXXx';
const REDIRECT_URI = 'localhost';
const TOKEN_FILENAME = 'ABC123.txt';

Properties and construct:

private $access_token;
private $refresh_token;

public function __construct()
{
    $tokens = $this->getTokens();
}

Get and set the tokens:

public function getTokens()
{
    $filename = self::TOKEN_FILENAME;
    if (!file_exists($filename)) {//First time use = create the file
        $this->createTokenFile();
    }
    $tokens = json_decode(file_get_contents($filename));
    $this->access_token = $tokens->access_token;
    $this->refresh_token = $tokens->refresh_token;
}

Create the token file (first time running):

public function createTokenFile()
{
    $data = json_decode($this->apiCall("" . self::TWITCH_OUATH_URI . "/token?client_id=" . self::CLIENT_ID . "&client_secret=" . self::CLIENT_SECRET . "&code=" . self::ACCESS_CODE . "&grant_type=authorization_code&redirect_uri=" . self::REDIRECT_URI . "", [], "POST"));
    $contents = '{"access_token": "' . $data->access_token . '", "refresh_token": "' . $data->refresh_token . '"}';
    $fp = fopen(self::TOKEN_FILENAME, 'w');
    fwrite($fp, $contents);
    fclose($fp);
}

Refresh the tokens:

public function refreshToken()
{
    $data = json_decode($this->apiCall("" . self::TWITCH_OUATH_URI . "/token?grant_type=refresh_token&refresh_token=$this->refresh_token&client_id=" . self::CLIENT_ID . "&client_secret=" . self::CLIENT_SECRET . "", [],'POST'));
    $contents = '{"access_token": "' . $data->access_token . '", "refresh_token": "' . $data->refresh_token . '"}';
    $fp = fopen(self::TOKEN_FILENAME, 'w');
    fwrite($fp, $contents);
    fclose($fp);
}

Check call was a success:

public function checkCallSuccess()
{
    if (isset($this->data['http_response_code']) && $this->data['http_response_code'] == 401) {
        $this->refreshToken();//Fetches new access and refresh tokens
        return false;
    } else {
        return json_encode($this->data);
    }
}

API call manager:

public function apiCall($url, $params = array(), $method = 'get')
{
    $data = null;
    $url = (self::URI . $url);
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_TIMEOUT, 5);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
        "Authorization: Bearer $this->access_token",
        "Client-ID: " . self::CLIENT_ID . ""
    ));
    if ($method == 'get' && !empty($params)) {
        $url = ($url . '?' . http_build_query($params));
    } else if ($method == 'post') {
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($params));
    }
    curl_setopt($curl, CURLOPT_URL, $url);
    $response = curl_exec($curl);
    $responseInfo = curl_getinfo($curl);
    if ($responseInfo['http_code'] == 200) {
        $data = $response;
    } else {
        $data = array('http_response_code' => $responseInfo['http_code']);//Call failed
    }
    $this->data = $data;
    return $data;
}

In the above apiCall() function if the response code isn’t 200 then an array will be returned, which states the response code. checkCallSuccess() checks the call and refreshes the token + returns false for a 401 code or returns the data if the error response isn’t found.

Covering your API calls to ensure that if the token is expired it will be refresh and re-run:

$data = $call->getUserLiveData('summit1g');
if (!$call->checkCallSuccess($data)) {//Call failed but refreshed token, try it again:
    $data = $call->getUserLiveData('summit1g');
}
echo $data;//Display the call

The above example uses the Twitch API Class, you will only need to run the checkCallSuccess() on one call per page as there is no point checking for an expired token if you refreshed it 1 second earlier.

$data = $call->getUserLiveData('summit1g');
if (!$call->checkCallSuccess($data)) {
    $data = $call->getUserLiveData('summit1g');
}
echo $data.'<br>;
echo $call->getTopStreams();//No need to check for expired token

You can now continuously use OAuth without worrying about refreshing tokens.