Add Custom Authenticator For Retrofit In Android To Handle JWT Authentication

You are currently viewing Add Custom Authenticator For Retrofit In Android To Handle JWT Authentication

In this post I will show you a very simple example of implementing a custom Authenticator in Retrofit to handle JWT authentication.

I created this is a demo environment that does not create a JWT. The JWT in this example is made of just a string. The post is mainly geared towards how to implement the actual logic of authentication in Retrofit. I plan on posting handling JWTs more in depth when I get to a good point in my project. I just need to continue porting the finalized implementation over.

The process of authentication here will go as follows:

  1. The request to access API is sent in a query. Tied to the query is the header tag, implemented by a token interceptor, that holds the user token.
  2. The server reads the token then decides
    • If the server, in my case I am working with a PHP server, reads the token and validates it, then the user query continues without issue
    • If the server denies the token as invalid, in my case for testing I used PHP http_response_code(401) for no access, then Retrofit hands the query to an Authenticator.

3. The authenticator then gets the token from the user app, Token Manager of some sort and sends its own query to the server for JWT validation or reissue.

Important

Inside the Authenticator class, you need to utilize runBlocking to block the current thread so the process of JWT exchange can finish first in the Authenticator class.

4. The response from the Authenticator will give you a token to then save in the Token Manager.

5. Now the main query will resume with the new token

The code in this project is just to show you basic implementation in a test environment.

Code

Token data type

data class Token (
    val token : String?
)

TokenManager here is just as stated above a string. This is just proof of concept for the Authenticator implementation.


class TokenManager : TokenInterface {

    private var token : String = "HEADER.PAYLOAD.SIGNATURE"

    override fun getToken() : String {
        return token
    }

    override fun setToken(newToken: String) {
        token = newToken
    }

}

interface TokenInterface {

    fun getToken() : String

    fun setToken(newToken : String)
}

The below snippet is my RetrofitHelper objects, the top is for all my queries and the bottom RetrofitHelperAuth is just for the Authenticator.

object RetrofitHelper {
    val baseUrl = "<serverAddress>/"
    fun getInstance(): Retrofit {
        val httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        val tokenManager = TokenManager()
        val tokenInterceptor = TokenInterceptor(tokenManager)
        val auth = TokenAuthenticator(tokenManager)
        val okHttpClient = OkHttpClient.Builder()
            .authenticator(auth)
            .addInterceptor(tokenInterceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()

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

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

object RetrofitHelperAuth {
    val baseUrl = "<serverAddress>/"
    fun getInstance(): Retrofit {
        val httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        val tokenManager = TokenManager()
        val tokenInterceptor = TokenInterceptor(tokenManager)
        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(tokenInterceptor)
            .addInterceptor(httpLoggingInterceptor)
            .build()

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

        return Retrofit.Builder()
            .baseUrl(baseUrl)
            //.addConverterFactory(nullOnEmptyConverterFactory)
            .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
    }
}

The TokenInterceptor

class TokenInterceptor(val tokenManager: TokenManager) : Interceptor {

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

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

The TokenAuthenticator

class TokenAuthenticator(val tokenManager : TokenManager) : Authenticator {

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

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

        val token = runBlocking {
            tokenManager.getToken()
        }

        return runBlocking {
            Log.d(TAG, token)
            // in real situation, change the below if statement. Used just to ensure it always queries for a new token
            if(token.length>1){
                val queryNewToken = RetrofitHelperAuth.getInstance().create(AccessApi::class.java)
                val queryResponse = queryNewToken.authtest()
                if(queryResponse.isSuccessful && queryResponse != null){
                    queryResponse.body()?.token?.let { tokenManager.setToken(it) }
                }
            }
            Log.d(TAG, tokenManager.getToken())
            response.request.newBuilder().header("Authorization", "Bearer ${tokenManager.getToken()}")
                .build()
        }
    }
}

This is my AccessAPI interface to hold the pages to query

interface AccessApi {

    @POST("testjwt.php")
    suspend fun altertestJWT(@Body message : String): Response<Token>

    @POST("authtest.php")
    suspend fun authtest(): Response<Token>


}

Sample ViewModel to call the above queries. In this demo I was lazy and put various classes together below. Not a good organizational way to do it but it works in a test environment.

class ViewModel(val applicationContext : Context) : ViewModel() {

    private val resultRepo = ResultRepo(applicationContext)

    var resultFlow = MutableStateFlow<ResultFromAPI>(ResultFromAPI.isLoading("Loading...."))

    fun getToken(){
        viewModelScope.launch {
            resultRepo.getJWT().collect{
                resultFlow.value = it
            }
        }
    }
}
    
sealed class ResultFromAPI {
    data class success(val data : String) : ResultFromAPI()
    data class error(val exception : String) : ResultFromAPI()
    data class isLoading(val message : String) : ResultFromAPI()
}

class ResultRepo(val context : Context){
    suspend fun getJWT() : Flow<ResultFromAPI> {
        return flow {
            emit(ResultFromAPI.isLoading("Loading response..."))
            delay(1000)
            val service = RetrofitHelper.getInstance(context).create(AccessApi::class.java)
            val response : Response<Token> = service.altertestJWT("Vegetables are healthy!")
            if(response.isSuccessful){
                val result = response.body()

                if(result == null){
                    emit(ResultFromAPI.error("Empty response..."))
                } else {
                    emit(ResultFromAPI.success(result.token.toString()))
                }
            } else {
                emit(ResultFromAPI.error("Empty response..."))
            }

        }.flowOn(Dispatchers.IO)
    }
}

class ViewModelFactory(val applicationContext : Context) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return if(modelClass.isAssignableFrom(com.itgeek25.samplejwt.viewmodel.ViewModel::class.java)){
            com.itgeek25.samplejwt.viewmodel.ViewModel(applicationContext) as T
        } else {
            throw IllegalArgumentException("ViewModel Not Found!!")
        }
    }
}

To view changes in the UI, this is the MainActivity

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val factory = ViewModelFactory(applicationContext)
        val viewModel = ViewModelProvider(this, factory)[ViewModel::class.java]

        enableEdgeToEdge()
        setContent {
            SampleJWTTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

                    var change by remember { mutableStateOf(0) }

                    var resultFromAPI by remember { mutableStateOf<ResultFromAPI>(ResultFromAPI.isLoading("")) }

                    LaunchedEffect(Unit) {
                        viewModel.resultFlow.collect{
                            resultFromAPI = it
                        }
                    }
                    
                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding),
                        horizontalAlignment = Alignment.CenterHorizontally,
                        verticalArrangement = Arrangement.Center
                    ) {
                        when(resultFromAPI){
                            is ResultFromAPI.error -> {
                                Text(text = (resultFromAPI as ResultFromAPI.error).exception)
                            }
                            is ResultFromAPI.isLoading -> {
                                Text(text = (resultFromAPI as ResultFromAPI.isLoading).message)
                            }
                            is ResultFromAPI.success -> {
                                Text(text = (resultFromAPI as ResultFromAPI.success).data)
                            }
                        }
                        Button(onClick = {
                            viewModel.getToken()
                            //change++

                        }) {
                            Text(text = "Update")
                        }
                    }
                }
            }
        }
    }
}

Now I will post a basic PHP script for the two addresses the implementation above query.

The first is the main query for “resources” the user may make, testjwt.php

<?php

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

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

     if($matches[1] !== "newCodeHeader.newCodePaylod.newCodeSignature"){
        http_response_code(401);
        exit;
     }
    echo json_encode(array(
        'token' => "Good job, it worked!" //"Type in your message here"
    ));
    exit;
} else {
    http_response_code(401);
    exit;
}

?>

The authtest.php

<?php

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

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

    if($matches[1] !== "newCodeHeader.newCodePaylod.newCodeSignature"){
        echo json_encode(array(
            'token' => "newCodeHeader.newCodePaylod.newCodeSignature"
        ));
    } else {
        echo json_encode(array(
            'token' => -1
        ));
        exit;
    }
} else {
    http_response_code(401);
    exit;
}

?>

I hope this was helpful on how to create and implement an Authenticator in Retrofit for JWT.

Leave a Reply