Working With JWT Authentication In Android And A PHP Server
I will show you a sample project of working with JWT Authentication in Android and a PHP Server.

This post will primarily be sharing the code I created to work with JWT authentication in Android communicating with a PHP server. This project is done in a test environment but is completely functional.

I don't plan on posting the UI logic, this is just the backend logic and implementation.

The PHP server holds the user credentials in a MySQL database.

The Android app just holds the access and refresh JWTs received from the PHP server on register or login in.

I set a short expiry on the access token and a long expiry on the refresh token. These can be changed to your liking.

The code could be shorten to make it a bit cleaner. I also have implemented asymmetric encryption for the user data being sent to the server.

Most of what I show here will have some explaining but is fairly straightforward especially if you have viewed my last few posts. These posts were parts of this project or built separately to be implemented in this project.

Implement JWT With Android and PHP Server

Code

Android Side

I will start with the Android code first. The names of each class or object is what I named them in the package. They are sorted in various folders for organization.

ErrorCodes

ErrorCodes - codes sent from PHP server in JSON, Android receives and processes the UI state based on these. I used negative numbers for ease of use and tracking. User ID's will always be positive.

data object ErrorCodes {
    val NO_ERROR : Int = -10
    val EMAIL_EXISTS : Int = -1
    val PROBLEM_ADDING_CREDENTIALS : Int = -2
    val ERROR_VALIDATING_CREDENTIALS : Int = -3
    val CONNECTION_ISSUE : Int = -4
    val FORM_DATA_ISSUE : Int = -5
    val NO_ACCESS : Int = -6
    val TOKEN_ERROR : Int = -9
}

JWT

JWT - data type to hold JWT in Retrofit response. The id is used for error handling

data class JWT(
    @SerializedName("id")
    val id : Int,
    @SerializedName("acc_token")
    val acc_token : String,
    @SerializedName("refresh_token")
    val refresh_token : String,
)

TokenSingleton

TokenSingleton - hold the JWT for user in datastore

class TokenSingleton {

    companion object {

        lateinit var dataStore: DataStore<Preferences>

        @Volatile
        private var INSTANCE: TokenSingleton? = null

        fun getInstance(context : Context): TokenSingleton? {
            if (INSTANCE == null) {
                synchronized(this) {
                    if (INSTANCE == null) {
                        // create the singleton instance
                        INSTANCE = TokenSingleton()
                        dataStore = PreferenceDataStoreFactory.create(
                            corruptionHandler = ReplaceFileCorruptionHandler(
                                produceNewData = { emptyPreferences() },
                            ),
                            produceFile = {
                                context
                                    .preferencesDataStoreFile("test_jwt_datastore")
                            }
                        )
                    }
                }
            }
            return INSTANCE
        }
    }

    suspend fun getAllInfo() : JWT {
        val prefs = dataStore.data.first()
        return JWT(-1, prefs[PrefKeys.ACC_TOKEN] ?: PrefKeys.EMPTY, prefs[PrefKeys.REFRESH_TOKEN] ?: PrefKeys.EMPTY)
    }

    suspend fun getAccToken() : String {
        val prefs = dataStore.data.first()
        return prefs[PrefKeys.ACC_TOKEN] ?: PrefKeys.EMPTY
    }

    suspend fun getRefreshToken() : String {
        val prefs = dataStore.data.first()
        return prefs[PrefKeys.REFRESH_TOKEN] ?: PrefKeys.EMPTY
    }

    suspend fun updateAccToken(acc_token : String){
        dataStore.edit {
            it[PrefKeys.ACC_TOKEN] = acc_token
        }
    }

    suspend fun updateRefreshToken(refresh_token : String){
        dataStore.edit {
            it[PrefKeys.REFRESH_TOKEN] = refresh_token
        }
    }

    suspend fun clearData(){
        dataStore.edit { it.clear() }
    }

}

private object PrefKeys {
    val ACC_TOKEN = stringPreferencesKey("acc_token")
    val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
    val EMPTY = "HEADER.PAYLOAD.SIGNATURE"
}

AccessAPI

AccessAPI - service used in Retrofit for the url and parameters to send as well as what daya types are being received. I had an issue with my PHP and communication here. The fix was to ensure all returned data types are of a list type. This is due to an issue in capturing the error codes from the Authenticator back to the original request for UI processing.

interface AccessApi {

    @POST("users_list.php")
    fun getUsers() : Call<List<UsersType>>

    @POST("register.php")
    fun registerUser(@Body userdata : UserData) : Call<JWT>

    @POST("login.php")
    fun loginUser(@Body userdata : UserData) : Call<JWT>

//used for Authenticator
    @POST("authtest.php")
    suspend fun jwtauth(): Response<List<JWT>>

UserData

UserData - data type to create object to create request to send to server. Used for login and register only. After JWT received, this is no longer used for authentication, just JWT.

data class UserData(
    @SerializedName("username")
    val username : String,
    @SerializedName("email")
    val email : String,
    @SerializedName("password")
    val password : String
)

JWTAuthenticator

JWTAuthenticator - this is what the JWTs use

class JWTAuthenticator(val applicationContext : Context) : Authenticator {

    private val TAG = JWTAuthenticator::class.java.name

    override fun authenticate(route: Route?, response: Response): Request {

        val token = runBlocking {
            Log.d(TAG, "Access: ${TokenSingleton.getInstance(applicationContext)!!.getAccToken()}")
            Log.d(TAG, "Refresh: ${TokenSingleton.getInstance(applicationContext)!!.getRefreshToken()}")
            TokenSingleton.getInstance(applicationContext)!!.getRefreshToken()
        }

        return runBlocking {
            if(token.length>1){
                val queryNewToken = RetrofitHelperAuth.getInstance(applicationContext).create(AccessApi::class.java)
                val queryResponse = queryNewToken.jwtauth()
                if(queryResponse.isSuccessful && queryResponse != null){
                    val result = queryResponse.body()
                    if(result != null && result.size > 0) {
                        result[0].acc_token.let { TokenSingleton.getInstance(applicationContext)!!.updateAccToken(it) }
                        result[0].refresh_token.let {
                            TokenSingleton.getInstance(applicationContext)!!.updateRefreshToken(
                                it
                            )
                        }
                    }
                    Log.d(TAG, "QueryResponse: ${queryResponse.body().toString()}")
                    Log.d(TAG, "Access: ${TokenSingleton.getInstance(applicationContext)!!.getAccToken()}")
                    Log.d(TAG, "Refresh: ${TokenSingleton.getInstance(applicationContext)!!.getRefreshToken()}")
                }
            }
            response.request.newBuilder().header("Authorization", "Bearer ${TokenSingleton.getInstance(applicationContext)!!.getAccToken()}")
                .build()
        }
    }
}

JWTAuthInterceptor

JWTAuthInterceptor - a separate interceptor used just for the JWTAuthenticator. This targets the refresh token

class JWTAuthInterceptor(val applicationContext : Context) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {


        val token = runBlocking { TokenSingleton.getInstance(applicationContext)!!.getRefreshToken() }
        val request = chain.request()

        val newRequest = request
            .newBuilder()
            .header("Authorization", "Bearer $token")
            .build()
        return chain.proceed(newRequest)
    }
}

JWTInterceptor

JWTInterceptor - this interceptor targets the use of the access token

class JWTInterceptor(val applicationContext : Context) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val token = runBlocking { TokenSingleton.getInstance(applicationContext)!!.getAccToken() }
        val request = chain.request()

        val newRequest = request
            .newBuilder()
            .header("Authorization", "Bearer $token")
            .build()
        return chain.proceed(newRequest)
    }
}

EncryptionInterceptor

EncryptionInterceptor - used to encrypt the data payload sent to server. The public key is saved in the R.raw resource named pk

class EncryptionInterceptor(val applicationContext : Context) : Interceptor {

    private val TAG : String = EncryptionInterceptor::class.java.simpleName

    override fun intercept(chain: Interceptor.Chain): Response {

        var request: Request = chain.request()
        var requestBody = request.body

        if (requestBody != null) {

            val buffer = Buffer()
            val mediaType: MediaType? = "application/x-www-form-urlencoded;charset=UTF-8".toMediaTypeOrNull() //text/plain; charset=utf-8
            requestBody.writeTo(buffer)

            val contentType = requestBody.contentType()
            val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8
            val strToEncrypt = buffer.readString(charset)
            Log.d(TAG, strToEncrypt)
            val secureMsg : String?
            if (strToEncrypt.isNotEmpty()) {
                secureMsg = processEncryptData(strToEncrypt)
            } else {
                Log.d(TAG, "{\"id\":500}")
                secureMsg = processEncryptData("{\"id\":500}")
            }
            if (secureMsg != null) {
                Log.d(TAG, secureMsg)
                val formattedStr = SecureDataMessage(secureMsg)
                val jsonObject = Gson().toJson(formattedStr)
                Log.d(TAG, jsonObject)
                val newRequestBody = RequestBody.create(mediaType, jsonObject)
                request = request.newBuilder().url(request.url).headers(request.headers).method(request.method, newRequestBody).build()
            }
        }



        Log.d(TAG, request.method)
        //request = request.newBuilder().build()
        return chain.proceed(request)
    }

    private fun processEncryptData(str : String) : String? {
        val inputStream: InputStream = applicationContext.resources.openRawResource(R.raw.pk)
        val bytes: ByteArray = inputStream.readBytes()
        val pk = String(bytes)
        Log.d("Public Key", pk)
        val encryptedData = SecureMessage.encryptData(str, pk)
        if (encryptedData != null) {
            Log.d("Encrypted Data", encryptedData)
            return encryptedData
        }
        return null
    }
}

SecureDataMessage

SecureDataMessage - data type for encryption

data class SecureDataMessage (

    @SerializedName("message")
    val message : String

)

SecureMessage

SecureMessage - object used to do encryption and decryption

object SecureMessage {

    fun getTheKey(pk : String) : String {
        val key = pk.replace("\\r".toRegex(), "")
            .replace("\\n".toRegex(), "")
            .replace(System.lineSeparator().toRegex(), "")
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .trim()
        Log.d("Public Key", key)
        return key
    }

    fun encryptData(txt: String, publicKey: String): String? {
        val pk = getTheKey(publicKey)
        var encoded = ""
        var encodedStr = StringBuilder()
        var encrypted: ByteArray? = null
        try {
            val publicBytes: ByteArray = Base64.decode(pk, Base64.DEFAULT)

            val keySpec = X509EncodedKeySpec(publicBytes)
            val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
            val pubKey: PublicKey = keyFactory.generatePublic(keySpec)
            val cipher: Cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING")
            cipher.init(Cipher.ENCRYPT_MODE, pubKey)

            val encryptTxt = txt.encodeToByteArray()
            Log.d("Encrypt Text", "Size of text to encrypt: " + encryptTxt.size.toString())

            val blockSize = 53
            val convertArray = arrayListOf<ByteArray>()
            val encryptedArray = arrayListOf<ByteArray>()
            var i = 0
            var index = 0
            var dstSize = blockSize
            if(encryptTxt.size < blockSize){
                encrypted = cipher.doFinal(encryptTxt)
                encoded = Base64.encode(encrypted, Base64.DEFAULT).decodeToString()
                return encoded
            }
            while (i < encryptTxt.size-1){
                Log.d("Encrypt Text", "$i : $dstSize")
                val dst = ByteArray(dstSize)
                while(index < dstSize){
                    dst[index] = encryptTxt[i]
                    index++
                    i++
                }
                index = 0

                convertArray.add(dst)
                encrypted = cipher.doFinal(dst)
                encryptedArray.add(Base64.encode(encrypted, Base64.DEFAULT))
                Log.d("Encrypt Text", "Sample: " + encryptedArray.get(encryptedArray.size-1).decodeToString())
                if(encryptTxt.size - 1 - i < blockSize){
                    dstSize = encryptTxt.size - i
                }
            }

            encodedStr.apply {
                encryptedArray.forEach {
                    if(this.isNotEmpty()){
                        this.append(":")
                    }
                    this.append(it.decodeToString().trim())
                }

            }
            return encodedStr.toString()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return encoded
    }
}

RetrofitHelper

RetrofitHelper - inside this class I have two different instances. This could be cleaned up a bit to use parameters so only one is needed. The top one is for regular requests. The bottom one is for the Authenticator. The main difference is which interceptor to use for what action.

object RetrofitHelper {
    val baseUrl = "<server_address>/"
    fun getInstance(applicationContext : Context): Retrofit {
        val httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

        val jwtInterceptor: JWTInterceptor = JWTInterceptor(applicationContext)
        val jwtAuthenticator = JWTAuthenticator(applicationContext)
        val encryptionInterceptor: EncryptionInterceptor = EncryptionInterceptor(applicationContext)
        val okHttpClient = OkHttpClient.Builder()
            .authenticator(jwtAuthenticator)
            .addInterceptor(jwtInterceptor)
            .addInterceptor(encryptionInterceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()

        val gson = GsonBuilder()
            .setLenient().create()

        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .client(okHttpClient)
            .build()
    }
}

object RetrofitHelperAuth {
    val baseUrl = "<server_address>/"
    fun getInstance(applicationContext : Context): Retrofit {
        val httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        val jwtAuthInterceptor: JWTAuthInterceptor = JWTAuthInterceptor(applicationContext)
        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(jwtAuthInterceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()

        val gson = GsonBuilder()
            .setLenient().create()

        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .client(okHttpClient)
            .build()
    }
}

val nullOnEmptyConverterFactory = object : Converter.Factory() {
    fun converterFactory() = this
    override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit) = object : Converter<ResponseBody, Any?> {
        val nextResponseBodyConverter = retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
        override fun convert(value: ResponseBody) = if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
    }
}

A sample how to call a request from the ViewModel

        val query = RetrofitHelper.getInstance(applicationContext).create(AccessApi::class.java)
        val call: Call<List<UsersType>> = query.getUsers()
        call.enqueue(object : Callback<List<UsersType>> {
            override fun onFailure(call: Call<List<UsersType>>, error: Throwable) {
             
            }

            override fun onResponse(
                call: Call<List<UsersType>>,
                response: Response<List<UsersType>>
            ) {
                val result = response.body()
                Log.i("Result", result?.size.toString())
                if (result != null) {
                // process data and send to a state for UI
                }
            }

UsersType

UsersType - the data type for the result of data received

data class UsersType(
    @SerializedName("id")
    val id : Int,
    @SerializedName("username")
    val username : String,
    @SerializedName("email")
    val email : String,
    @SerializedName("acc_token")
    val acc_token : String
)

PHP Side

Now the PHP side. I am posting just snippets of various actions that would be most important for you to implement.

A great deal of the PHP logic was created by using the following as a reference. Good deal and explanations.

https://www.freecodecamp.org/news/php-jwt-authentication-implementation

JWT

This is the JWT.php - hold functions to encode and decode the JWT

<?php

class JWT
{

    public function __construct(private string $key)
    {

    }


    private function base64URLEncode(string $text): string
    {

        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($text));
    }

    public function encode(array $payload): string
    {

        $header = json_encode([
            "alg" => "HS256",
            "typ" => "JWT"
        ]);

        $header = $this->base64URLEncode($header);
        $payload = json_encode($payload);
        $payload = $this->base64URLEncode($payload);

        $signature = hash_hmac("sha256", $header . "." . $payload, $this->key, true);
        $signature = $this->base64URLEncode($signature);
        return $header . "." . $payload . "." . $signature;
    }

    public function decode(string $token): array
    {
        if (
            preg_match(
                "/^(?<header>.+)\.(?<payload>.+)\.(?<signature>.+)$/",
                $token,
                $matches
            ) !== 1
        ) {

            throw new InvalidArgumentException("invalid token format");
        }

        $signature = hash_hmac(
            "sha256",
            $matches["header"] . "." . $matches["payload"],
            $this->key,
            true
        );

        $signature_from_token = $this->base64URLDecode($matches["signature"]);

        if (!hash_equals($signature, $signature_from_token)) {

            // throw new Exception("signature doesn't match");
            throw new InvalidArgumentException("Invalid Signature");
        }

        $payload = json_decode($this->base64URLDecode($matches["payload"]), true);

        return $payload;
    }

    private function base64URLDecode(string $text): string
    {
        return base64_decode(
            str_replace(
                ["-", "_"],
                ["+", "/"],
                $text
            )
        );
    }

}

?>

ErrorCode

<?php

class ErrorCodes {
    const NO_ERROR = -10;
    const EMAIL_EXISTS = -1;
    const PROBLEM_ADDING_CREDENTIALS = -2;
    const ERROR_VALIDATING_CREDENTIALS = -3;
    const CONNECTION_ISSUE = -4;
    const FORM_DATA_ISSUE = -5;
    const NO_ACCESS = -6;
    const TOKEN_ERROR = -9;
    
}
?>

MyUtils

<?php

class MyUtils {

    function getTodayDate(){
        return date("Y-m-d");
    }

    function getTimestampFromDate($date){
        return strtotime($date);
    }

    function findDifferenceInDaysBetweenTimeStamps($dateOne, $dateTwo){
        return ($dateOne - $dateTwo) / (60 * 60 * 24);
    }

    function findDifferenceInMinutesBetweenTimeStamps($dateOne, $dateTwo){
        return ($dateOne - $dateTwo) / (60);
    }

    function decryptPost($message){

        $dataArr = explode(":", $message);

        $str = "";

        for($i = 0; $i < count($dataArr); $i++){

            openssl_private_decrypt(base64_decode($dataArr[$i]), $test_decrypted, $_ENV['test_pk']);

            $str .= $test_decrypted;

        }

        $str = mb_convert_encoding($str, "UTF-8");

        $json = json_decode($str, true);

        return $json;
    }

    function decryptString($message){

        $dataArr = explode(":", $message);

        $str = "";

        for($i = 0; $i < count($dataArr); $i++){

            openssl_private_decrypt(base64_decode($dataArr[$i]), $test_decrypted, $_ENV['test_pk']);

            $str .= $test_decrypted;

        }

        $str = mb_convert_encoding($str, "UTF-8");

        return $str;
    }

    function encryptPost($encryptedText){

        $encrypted_arr = str_split($encryptedText, 53);

        $encryptedStr = "";

        for($i = 0; $i < sizeof($encrypted_arr); $i++){

            openssl_public_encrypt($encrypted_arr[$i], $encrypted, $_ENV['test_pubk']);

            if($i != 0){
                $encryptedStr .= ":";
            }
            $encryptedStr .= base64_encode($encrypted);
        }
        return $encryptedStr;
    }

}

?>

.env

.env file used to store access variables

*DB_HOST=localhost
*DB_NAME=database_name
*DB_USER=admin
*DB_PASS=password
*SECRET_KEY=secret_key
*test_pubk=-----BEGIN PUBLIC KEY-----
public-key
public-key
-----END PUBLIC KEY-----
*test_pk=-----BEGIN PRIVATE KEY-----
private-key
private-key
private-key
private-key
private-key
private-key
private-key
private-key
-----END PRIVATE KEY----

DotEnv

DotEnv.php - used to access .env variables

<?php

class DotEnv
{
    /**
     * The directory where the .env file can be located.
     *
     * @var string
     */
    protected $path;


    public function __construct(string $path)
    {
        if(!file_exists($path)) {
            throw new \InvalidArgumentException(sprintf('%s does not exist', $path));
        }
        $this->path = $path;
    }

    //require_once("DotEnv.php");
    //(new DotEnv(__DIR__ . '/.env'))->load();

    public function load() :void
    {
        if (!is_readable($this->path)) {
            throw new \RuntimeException(sprintf('%s file is not readable', $this->path));
        }

        $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        $currentVal = "";
        $currentName = "";
        $stillProcessingLine = 0; // false = 0, true = 1
        for($i = 0; $i < sizeof($lines); $i++){

            if($stillProcessingLine){
                $currentVal .= "\n".$lines[$i];
            }
            if(strpos($lines[$i], '*') === 0){
                $stillProcessingLine = 0;
                $arr = explode('=', $lines[$i], 2);
                $currentName = $arr[0];
                $currentVal = $arr[1];
                $currentName = str_replace("*", "", $currentName);
                
                
                
            }
            if($i < sizeof($lines)-1){
                if(strpos($lines[$i+1], "*") === false){
                    $stillProcessingLine = 1;
                }else {
                    $stillProcessingLine = 0;
                }
            } else {
                $stillProcessingLine = 0;
            }

            if($stillProcessingLine === 0){

            $name = trim($currentName);
            $value = trim($currentVal);

            if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
                putenv(sprintf('%s=%s', $name, $value));
                $_ENV[$name] = $value;
                $_SERVER[$name] = $value;
            }
            }
        }
    }

}

?>

Database Connection

<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(403);
    exit;
}
//load the .env file
require_once("DotEnv.php");

(new DotEnv(__DIR__ . '/../.env'))->load();

require_once("MyUtils.php");
require_once("ErrorCodes.php");

class Database
{
    private ?PDO $conn = null;

    public function __construct(
        private string $host,
        private string $name,
        private string $user,
        private string $password
    ) {
    }

    public function getConnection(): ?PDO
    {
        try {
            if ($this->conn === null) {
                $this->conn = new PDO("mysql:host={$this->host};dbname={$this->name}", $this->user, $this->password);
                $this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $this->conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
                $this->conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
            }
        
            return $this->conn;
        } catch (PDOException $e) {
            echo "Connection failed: " . $e->getMessage();
            return null;
        }
    }

}

?>

Sample of connecting to the database

    $errorCodes = new ErrorCodes();

    $database = new Database($_ENV['DB_HOST'], $_ENV['DB_NAME'], $_ENV['DB_USER'], $_ENV['DB_PASS']);
    $con = $database->getConnection();

User Database Query

The process now would be to find the user in the database from login/register. Then implement the follow after you captured the user in a search query. As of now I just wanted to add a random tag in the JWT payload. You can put whatever you want in it.

if($find_user->rowCount() == 1){
                    $row = $find_user->fetch();
                    $current_time = $myUtils->getTimestampFromDate($myUtils->getTodayDate());
                    $JWTController = new JWT($_ENV['SECRET_KEY']);
        
                    $my_rand = sha1(uniqid().rand(1000000, 9999999));
                    $acc_payload = array(
                        'id' => $row['id'],
                        'username' => $row['username'],
                        'job' => $my_rand,
                        'iat' => $current_time
                    );
    
                    $acc_token = $JWTController->encode($acc_payload);

I would do the same for the refresh token then add both of these to the database in the users row.

IMPORTANT

Now for queries that validate the JWT. At any point where the data / token is invalid, you can send the

http_response_code(401);

This is what calls the Android Authenticator into action.

<?php

if (!isset($_SERVER["HTTP_AUTHORIZATION"])) {

    http_response_code(403);
    exit;
}

require_once('ErrorCodes.php');
$errorCodes = new ErrorCodes();

if (!preg_match("/Bearer\s(\S+)/", $_SERVER["HTTP_AUTHORIZATION"], $matches)) {
    echo json_encode(array(
        'id' => $errorCodes::NO_ACCESS
    ));
    exit;
}

if(sizeof($matches) > 1){

//open database connection here

<?php

if (!isset($_SERVER["HTTP_AUTHORIZATION"])) {

    http_response_code(403);
    exit;
}

// echo json_encode(array(
//     'token' => $_SERVER["HTTP_AUTHORIZATION"] //"Type in your message here"
// ));

// exit;

require_once('ErrorCodes.php');
$errorCodes = new ErrorCodes();

if (!preg_match("/Bearer\s(\S+)/", $_SERVER["HTTP_AUTHORIZATION"], $matches)) {
    echo json_encode(array(
        'id' => $errorCodes::NO_ACCESS
    ));
    exit;
}

if(sizeof($matches) > 1){

            $id = $payload['id'];
            $username = $payload['username'];
            $acc_token = $matches[1];
            
            //search the 3 parameters above in the database. if it returns a single row, then you have the right user and can proceed
            
            if($find_user->rowCount() == 1){
                    $row = $find_user->fetch();
                    $current_time = $myUtils->getTimestampFromDate($myUtils->getTodayDate());
                    
                    $timestamp = $row['timestamp'];
                    $diff = $myUtils->findDifferenceInMinutesBetweenTimeStamps($current_time, $timestamp);
                    
                    if($diff > 30){
                    //this part is what calls the Android Retrofit Authenticator
                        http_response_code(401);
                        exit;
                    }
                    
                    }
            
            

}


}

Decryption in PHP

This is where the JWT authentication starts on the PHP side.

        $myUtils = new MyUtils();
        //get the encrypted message sent via POST
        $data = json_decode(file_get_contents('php://input'));       

        $message = $data->{'message'};
        //return decrypted message as JSON
        $json = $myUtils->decryptPost($message);

Authenticator In PHP

Lastly the authenticator php file. It would be also like above code will it check the refresh token instead of the access token. After it validates its correct, then it will create a new access token ( also a new refresh token if out of date ) and send out through JSON just like what you register a new user.

<?php

if (isset($_SERVER["HTTP_AUTHORIZATION"])) {

    require_once('ErrorCodes.php');
    $errorCodes = new ErrorCodes();

    if (!preg_match("/Bearer\s(\S+)/", $_SERVER["HTTP_AUTHORIZATION"], $matches)) {

        echo json_encode(array(
            'id' => $errorCodes::NO_ACCESS
        ));
        exit;
    }

    if(sizeof($matches) > 1){
    
            $JWTController = new JWT($_ENV['SECRET_KEY']);
        $payload = null;
        try{
            $payload = $JWTController->decode($matches[1]);
        } catch(Throwable $e){
    
        }
    
        if($payload != null){
                    $refresh_token = $matches[1];

            $id = $payload['id'];
            $username = $payload['username'];
        //open database and search for the user row using the above 3 parameters
        
        //check age of refresh token, if old create new
        if($find_user->rowCount() == 1){
                    $current_time = $myUtils->getTimestampFromDate($myUtils->getTodayDate());
                    $row = $find_user->fetch();

                    $diff = $myUtils->findDifferenceInDaysBetweenTimeStamps($current_time, $payload['iat']);
                    
                    if($diff > 4){
                        $my_token = sha1(uniqid().rand(1000000, 9999999));
                        $refresh_payload = array(
                            'id' => $row['id'],
                            'username' => $row['username'],
                            'job' => $my_token,
                            'iat' => $current_time
                        );
    
                        $refresh_token = $JWTController->encode($refresh_payload);
                    }
                    
                    //lastly update database with timestamp, access and refresh tokens. then create a JSON array to send back to the authenticator
                    echo json_encode(array(
                        array(
                        'id' => $errorCodes::NO_ERROR,
                        'acc_token' => $row['acc_token'],
                        'refresh_token' => $row['refresh_token']
                        )
                    ));
                    
                    
                    }
        
        }
    
    
    }

In the PHP I skipped a bunch of generic code to keep things short. I also didn't show all my uses of error / exception identification. You can go through this code and find various spots to put them. Below shows an example of how I needed to send it so Android via the List<DataObject> would receive it. Its basically an array inside of an array.

echo json_encode(array(
                        array(
                        'id' => $errorCodes::TOKEN_ERROR
                        )
                    ));

Hope this helps.