Search code examples
androidretrofit2okhttp

How to check token expiration at interceptors android retrofit?


I would like to handle token expiration by myself and send request for new tokens. I have such condition:

sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60

This condition checks when my token will become expired and I have to send a new request from interceptor. I saw this question also. I have created such interceptor:

class RefreshTokens(cont: Context) : Interceptor{
    val context = cont
    override fun intercept(chain: Interceptor.Chain): Response {
        var tokenIsUpToDate = false
        val sp = context.getSharedPreferences("app_data", 0)
        if (sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60) {
            Singleton.apiService(context).getNewToken(ReqAccessToken(context.getSharedPreferences("app_data", 0).getString("refresh_token", ""))).enqueue(object : Callback<ResNewTokens>, retrofit2.Callback<ResNewTokens> {
                override fun onResponse(call: Call<ResNewTokens>, response: retrofit2.Response<ResNewTokens>) {
                    if (response.isSuccessful) {
                        tokenIsUpToDate = true
                    }
                }

                override fun onFailure(call: Call<ResNewTokens>, t: Throwable) {

                }

            })

            return if (tokenIsUpToDate) {
                chain.proceed(chain.request())
            } else {
                chain.proceed(chain.request())
            }

        } else {
            val response = chain.proceed(chain.request())
            when (response.code) {
                401->{
                    chain.request().url
                    response.request.newBuilder()
                            .header("Authorization", "Bearer " + context.getSharedPreferences("app_data", 0).getString("access_token", "")!!)
                            .build()
                }
                500 -> {
                    Toast.makeText(context, context.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
                }
            }
            return response
        }
    }
}

I can't imagine how to add return condition to my code. I know about Authentificator but when I use it I send one more request which response gives me 401 error for token updating. When I use Authentificator I send such requests:

  1. Request with old access_token -> 401 error
  2. Request for the new tokens -> 200 OK
  3. Request with new access_token -> 200 OK

So I would like to remove 1 request which will give error and send request for a new tokens. But I have to problems:

  1. I don't know how to fix my interceptor for solving this task
  2. I don't know how to repeat request which I was going to make like in Authentificator

Maybe someone knows how to solve my problem?


Solution

  • I would like to share my own solution which works good as I see:

    class AuthToken(context: Context) : Interceptor {
        var cont = context
        override fun intercept(chain: Interceptor.Chain): Response {
            val sp = cont.getSharedPreferences("app_data", 0)
            if (sp!!.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60 && !sp.getString("refresh_token", "")!!.isBlank()) updateAccessToken(cont)
    
            val initialRequest = if (sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60 && !sp.getString("refresh_token", "")!!.isBlank()) {
                updateAccessToken(cont)
                requestBuilder(chain)
            } else {
                requestBuilder(chain)
            }
    
    
            val initialResponse = chain.proceed(initialRequest)
    
            return if (initialResponse.code == 401 && !sp.getString("refresh_token", "").isNullOrBlank() && sp.getLong("expires_in", 0) - sp.getLong("time_delta", 0) - System.currentTimeMillis() / 1000 <= 60) {
                updateAccessToken(cont)
                initialResponse.close()
                val authorizedRequest = initialRequest
                        .newBuilder()
                        .addHeader("Content-type:", "application/json")
                        .addHeader("Authorization", "Bearer " + cont.getSharedPreferences("app_data", 0).getString("access_token", "")!!)
                        .build()
                chain.proceed(authorizedRequest)
            } else {
                val errorBody = initialResponse.message
                when {
    
                }
                if (initialResponse.code == 500) {
                    val thread = object : Thread() {
                        override fun run() {
                            Looper.prepare()
                            Toast.makeText(cont, cont.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
                            Looper.loop()
                        }
                    }
                    thread.start()
                }
                initialResponse
            }
        }
    
    
        private fun updateAccessToken(context: Context) {
            val sp = context.getSharedPreferences("app_data", 0)
            synchronized(this) {
                val tokensCall = accessTokenApi()
                        .getNewToken(ReqAccessToken(context.getSharedPreferences("app_data", 0).getString("refresh_token", "")!!))
                        .execute()
    
                if (tokensCall.isSuccessful) {
                    val responseBody = tokensCall.body()
                    val editor = sp.edit()
    
                    val localTime = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH).parse(tokensCall.headers()["Date"]!!)
                    Singleton.setServerTime(localTime!!.time / 1000, context)
    
                    editor.putString("access_token", Objects.requireNonNull<ResNewTokens>(responseBody).access_token).apply()
                    editor.putString("refresh_token", Objects.requireNonNull<ResNewTokens>(responseBody).refresh_token).apply()
                    editor.putLong("expires_in", responseBody!!.expires_in!!).apply()
                } else {
                    when (tokensCall.code()) {
                        500 -> {
                            val thread = object : Thread() {
                                override fun run() {
                                    Looper.prepare()
                                    Toast.makeText(cont, cont.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
                                    Looper.loop()
                                }
                            }
                            thread.start()
                        }
    
                        401 -> {
                            Singleton.logOut(context)
                        }
                    }
                }
    
            }
        }
    
    
        private fun requestBuilder(chain: Interceptor.Chain): Request {
            return chain.request()
                    .newBuilder()
                    .header("Content-type:", "application/json")
                    .header("Authorization", "Bearer " + cont.getSharedPreferences("app_data", 0).getString("access_token", "")!!)
                    .build()
        }
    
        private fun accessTokenApi(): APIService {
            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY
    
            val dispatcher = Dispatcher()
            dispatcher.maxRequests = 1
    
    
            val client = OkHttpClient.Builder()
                    .addInterceptor(interceptor)
                    .connectTimeout(100, TimeUnit.SECONDS)
                    .dispatcher(dispatcher)
                    .readTimeout(100, TimeUnit.SECONDS).build()
    
    
            client.dispatcher.cancelAll()
    
            val retrofit = Retrofit.Builder()
                    .baseUrl(BuildConfig.API_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
    
            return retrofit.create(APIService::class.java)
        }
    }
    

    In general as I see I send request for token refreshing before send request with expired access_token. Maybe someone will have some suggestions or improvements for my solution :)