Retrofit interceptor for JWT datastore in jetpack compose

Issue

Sorry new to jetpack compose here,

My interceptor for retrofit, uses old JWT value while making api calls from datastore.
On relaunch it fetches latest and correct. But just after login, it uses old value of JWT from datastore. Why?

interface SplitFoolContainer {
    val accountServiceRepository: AccountServiceRepository
    val googleAuthUiClient: GoogleAuthUiClient
    val dataStoreRepository:DataStoreRepository
    val fellowRepository:FellowServiceRepo
}





class TokenInterceptor(private val tokenData:String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {



        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $tokenData")
            .build()
        return chain.proceed(request)
    }
}

class AuthAuthenticator(private val context: Context):Authenticator{
    override fun authenticate(route: Route?, response: Response): Request? {
        val currentToken = runBlocking {
            context.dataStore.data.map { preferences ->
                preferences[stringPreferencesKey("is_jwt_token")] ?: ""
            }.first()
        }

        Log.d("AuthAuthenticator", "authenticate: $currentToken")
       synchronized(this){
           val updatedToken = runBlocking {
               context.dataStore.data.map { preferences ->
                   preferences[stringPreferencesKey("is_jwt_token")] ?: ""
               }.first()
           }

           Log.d("AuthAuthenticator", "updated: $updatedToken")

           return if (updatedToken != currentToken) response.request.newBuilder()
               .header("Authorization", "Bearer $updatedToken")
               .build() else null
       }


    }
}


private const val JWT_STORED_DATA_STORE = "JWT_STORED_DATA_STORE"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = JWT_STORED_DATA_STORE
)

/**
 * [AppContainer] implementation that provides instance of [BusRepository]
 */
class SplitFoolDataContainer(private val context: Context) : SplitFoolContainer {
    /**
     * Implementation for [BusRepository]
     */


    private val baseUrl = "MYURL"


    private fun createOkHttpClient(): OkHttpClient {

        val map = runBlocking {
            context.dataStore.data.map { preferences ->
                preferences[stringPreferencesKey("is_jwt_token")] ?: ""
            }.first()
        }



        Log.d("great gaurav", "createOkHttpClient: $map")

        val tokenInterceptor = TokenInterceptor(tokenData = map)

        // Return OkHttpClient with the token interceptor
        return OkHttpClient.Builder()
//            .authenticator(AuthAuthenticator(context))
            .addInterceptor(tokenInterceptor)
            .build()

    }



    private val retrofit: Retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(baseUrl)
        .client(createOkHttpClient())
        .build()

    private val loginApiService: LoginService by lazy {
        retrofit.create(LoginService::class.java)
    }

    private val fellowApiService: FellowApiService by lazy {
        retrofit.create(FellowApiService::class.java)
    }

    override val googleAuthUiClient by lazy {
        Log.d( "SplitFoolDataContainer", "googleAuthUiClient: ${context.dataStore.data}")
        GoogleAuthUiClient(
            context = context,
            oneTapClient = Identity.getSignInClient(context)
        )
    }




    override val dataStoreRepository: DataStoreRepository by lazy {
        DataStoreRepository(
            dataStore = context.dataStore
        )
    }

    override val fellowRepository: FellowServiceRepo by lazy {
        FellowServiceRepo(
            fellowApiService = fellowApiService
        )
    }


}

my SplitContainer is the container of my app. Letme know if you want any inputs from me?

Solution

Based on your code snippet i think that problem is that your TokenInterceptor doesn’t know about token change. You are creating one instance of TokenInterceptor once with token value available on app start (or when SplitFoolDataContainer is created). So when you login and get new token, you are still using same interceptor instance with old token.

On next launch it works okay because TokenInterceptor is created with actual token which was saved before on the login.

EDIT:
As quick workaround you can collect token in the TokenInterceptor.

  class TokenInterceptor(dataStore: DataStore<Preferences>) : Interceptor {
//Holding actual token data
private var tokenData: String = ""

init {
    val tokenFlow = dataStore.data.map {
        it[stringPreferencesKey("is_jwt_token")]
    }

    CoroutineScope(Dispatchers.IO).launch {
        //Collecting token value
        tokenFlow.collect { newToken -> 
            //Storing new token value
            tokenData = newToken ?: "" 
        }
    }
}

override fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request().newBuilder()
        .addHeader("Authorization", "Bearer $tokenData")
        .build()
    return chain.proceed(request)
   }
}

As i said this is just quick workaround, i am not sure if dataStore is best for storing tokens because these collecting problems, i would much rather go with SharedPreferences.

Answered By – Miroslav Hýbler

Answer Checked By – Terry (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *