We are moving our backend to Firebase tokens, so I tried updating the Android app with the following code. We use Kotlin/Retrofit/OkHttp to make request. In order to add tokens correctly to all HTTP requests to the server I've created an interceptor in the following way:
class FirebaseAuthInterceptor: Interceptor {
private var token: String = ""
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val user = FirebaseAuth.getInstance().currentUser
if (user != null) {
runBlocking {
val tkn = getBearerToken(user)
token = "Bearer $tkn"
}
}
request = request.newBuilder().addHeader("authorization", token.trim()).build()
return chain.proceed(request)
}
#OptIn(ExperimentalCoroutinesApi::class)
private suspend fun getBearerToken(user: FirebaseUser): String =
withTimeout(TIMEOUT_MS) {
suspendCancellableCoroutine { emitter ->
user.getIdToken(true)//true
.addOnSuccessListener {
emitter.resume(it.token.toString()) {}
}
.addOnCanceledListener {
emitter.cancel(IllegalStateException("getIdToken canceled"))
Timber.e("requestResult: getIdToken cancelled")
}
.addOnFailureListener {
emitter.cancel(UnknownHostException())
Timber.e("requestResult: getIdToken failed")
}
}
}
}
However, it looks like withTimeout expires A LOT (according to the logs in our Firebase Crashlytics: 35 crash events affecting 22 users in the last 12h). I see the following error:
Fatal Exception: ck.x2: Timed out waiting for 60000 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
Searching through the web, there really isn't any recommended way of going about this. What is the best way of passing the bearer token to HTTP requests, and always having a fresh one available on the device when the calls are made.
Thank you
Related
What is the better approach to regenerate the JWT token using refresh token in Apollo Graphql in Andriod Kotlin?.
Now we are using an Auth interceptor that extends ApolloInterceptor to attach the JWT token in the request header.
I want to check whether the Toke expires from here and it should call another mutation to generate a new token. Then it should proceed with the previous call with the new token.
Please refer to the code below
class AuthInterceptor(private val jwtToken: String) : ApolloInterceptor {
override fun interceptAsync(
request: ApolloInterceptor.InterceptorRequest,
chain: ApolloInterceptorChain,
dispatcher: Executor,
callBack: ApolloInterceptor.CallBack
) {
val header = request.requestHeaders.toBuilder().addHeader(
HEADER_AUTHORIZATION,
"$HEADER_AUTHORIZATION_BEARER $jwtToken"
).build()
chain.proceedAsync(request.toBuilder().requestHeaders(header).build(), dispatcher, callBack)
}
override fun dispose() {}
companion object {
private const val HEADER_AUTHORIZATION = "Authorization"
private const val HEADER_AUTHORIZATION_BEARER = "Bearer"
}
}
If you are using Apollo Android Client V3 and Using Kotlin Coroutine then use Apollo Runtime Dependency and Try HttpInterceptor instead of ApolloInterceptor. I think this is the better/best approach. For Reference Click Here
In your app-level build.gradle file
plugins {
......
id("com.apollographql.apollo3").version("3.5.0")
.....
}
dependencies {
implementation("com.apollographql.apollo3:apollo-runtime")
}
Now write your interceptor for the Apollo client.
FYI: If you've added the Authorization header using Interceptor or using addHttpHeader in client already then remove it or don't add header here val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build()), just build the request. Otherwise Authorization header will add multiple times in the request. So, be careful.
class AuthorizationInterceptor #Inject constructor(
val tokenRepo: YourTokenRepo
) : HttpInterceptor {
private val mutex = Mutex()
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
var token = mutex.withLock {
// get current token
}
val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
return if (response.statusCode == 401) {
token = mutex.withLock {
// get new token from your refresh token API
}
chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
} else {
response
}
}
}
Configure your Apollo client like below.
ApolloClient.Builder()
.httpServerUrl(BASE_GRAPHQL_URL)
.webSocketServerUrl(BASE_GRAPHQL_WEBSOCKET_ENDPOINT) // if needed
.addHttpHeader("Accept", "application/json")
.addHttpHeader("Content-Type", "application/json")
.addHttpHeader("User-Agent", userAgent)
.addHttpInterceptor(AuthorizationInterceptor(YourTokenRepo))
.httpExposeErrorBody(true)
.build()
I'm adding DI to the existing project, in process I faced problem that header Authorization disappears from request. There is no any exceptions or logs from Retrofit/OkHttp. My dependencies are:
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.okhttp:okhttp:2.7.5'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'org.koin:koin-android:2.1.3'
I create http client using provideClient:
class OkHttpProvider private constructor() {
companion object {
fun provideClient(credentials: UsernamePasswordCredentials? = null, context: Context): OkHttpClient {
val client = OkHttpClient.Builder()
// logs
if (BuildConfig.DEBUG) {
client.addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
)
}
if (credentials != null) {
val creds = Credentials.basic(credentials.userName, credentials.password)
val headerInterceptor = Interceptor { chain ->
var request = chain.request()
val headers = request
.headers()
.newBuilder()
.add("Authorization", creds)
.build()
request = request.newBuilder().headers(headers).build()
chain.proceed(request)
}
//client.addInterceptor(AccessTokenInterceptor(credentials))
client.addInterceptor(headerInterceptor)
}
client
.callTimeout(60L, TimeUnit.SECONDS)
.connectTimeout(10L, TimeUnit.SECONDS)
.readTimeout(60L, TimeUnit.SECONDS)
.writeTimeout(60L, TimeUnit.SECONDS)
.sslSocketFactory(getSslContext().socketFactory).hostnameVerifier { _, _ -> true }
client.addInterceptor(ChuckInterceptor(context))
return client.build()
}
private fun getSslContext(): SSLContext {
...implementation...
}
}
}
My modules for http client and Retrofit are below:
object HttpClientModule {
val module = module {
single(named(COMMON)) {
OkHttpProvider.provideClient(
get<SharedPreferenceManager>().getUserCredentials(),
androidContext()
)
}
...other versions...
}
const val COMMON = "common"
}
object ApiModule {
val module = module {
single {
RetrofitFactory.getServiceInstance(
ApiService::class.java,
get<SharedPreferenceManager>().getString(LocalDataSource.BUILD_OPTION_API, ""),
get(named(HttpClientModule.COMMON))
)
}
...other apis...
}
}
object RetrofitFactory {
const val GEO_URL = "http://maps.googleapis.com/maps/api/"
fun <T> getServiceInstance(
clazz: Class<T>,
url: String = GEO_URL,
client: OkHttpClient
): T = getRetrofitInstance(url, client).create(clazz)
private fun getRetrofitInstance(
url: String,
client: OkHttpClient
) = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
}
App starts to work with "admin" user and has some credentials saved in shared preferences, when user starts login with phone and sms and requests are sent with "admin" Authorization header, when user inputs code from sms and his new user credentials are saved in shared preferences. After that app sends two requests and Authorization header isn't presented in them. I saw it in Chuck, I even rechecked it using Charles.
To fix this problem I tried few solutions. Firstly, I changed inject for http client from single to factory, that didn't work. Secondly, I googled the problem, but I didn't mentions of this phenomenon. Thirdly, I wrote AccessTokenInterceptor according to this article and also cover everything with logs. I noticed that interceptor works fine in normal cases, but when Authorization header is missing method intercept is not called. This might be reason why default headerInterceptor also not working. Fourthly, I upgraded versions of Retrofit and OkHttp, this also didn't helped.
I noticed interesting thing about that bug: if I restart app after Retrofit lost Authorization header, app works fine test user is properly logged with correct token. Any attempts to relog without restarting the app fails. Maybe someone had similar problem or knows what is happening here, any ideas are welcomed.
I finally find solution to this problem. The problem was user credentials was passed to provideClient only once, when it's created. At that moment user was logged as admin, and standard user credentials was empty, so http client for ApiService was created without Authorization header.
To solve this I changed AccessTokenInterceptor form article (HttpClientType is a enum to select which credentials need to use):
class AccessTokenInterceptor(
private val sharedPreferenceManager: SharedPreferenceManager,
private val clientType: OkHttpProvider.HttpClientType
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val credentials = getUserCredentials(clientType)
if (credentials != null) {
val accessToken = Credentials.basic(credentials.userName, credentials.password)
val request = newRequestWithAccessToken(chain.request(), accessToken)
return chain.proceed(request)
} else {
return chain.proceed(chain.request())
}
}
private fun getUserCredentials(clientType: OkHttpProvider.HttpClientType): UsernamePasswordCredentials? {
return when (clientType) {
OkHttpProvider.HttpClientType.COMMON -> sharedPreferenceManager.getUserCredentials()
OkHttpProvider.HttpClientType.ADMIN -> ServiceCredentialsUtils.getCredentials(sharedPreferenceManager)
}
}
private fun newRequestWithAccessToken(#NonNull request: Request, #NonNull accessToken: String): Request {
return if (request.header("Authorization") == null) {
request.newBuilder()
.header("Authorization", accessToken)
.build()
} else {
request
}
}
}
Now each time request is sending, Interceptor gets user's credentials and adds header to request.
I have a basic Retrofit setup for network requests. I have the following Authenticator that is added to the chain. It basically tries to refresh access token when authorization error (401) occurs.
class TokenAuthenticator(private val api: MyApi) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val retrofitResponse = api.refreshAccessToken("my refresh token here"))
val refreshResponse= retrofitResponse.blockingGet()
return if(refreshResponse != null) {
response.request().newBuilder()
.header(Const.HEADER_AUTHORIZATION, "Bearer " + refreshResponse.accessToken)
.build()
} else {
return null
}
}
}
The problem is my server might return 401 not only for authroization issues but also for some other cases. For example, i might get response with 401 if user phone number already exists in the database. Server returns me error_code paramter to differentiate this type of issues:
error_code = "token_expired" -> authoriation issue. shows that access token expired.
error_code = "phone_exists" -> shows phone number entered already exsits in the database.
So, I need to be able to check for this paramter before deciding that error was access token refresh error. How can I do that?
Currently, since I have not been able to check for that paramter, my app thinks that 401 is an authroization issue and continuously trying to refresh the access token even though my access token is not expired.
we had similar issue in our project, it may be a little mess because of hard-coded url but i think it is okey
we check the request's url and if it matched with the refresh token url then we start process of getting new token
Do you mean ErrorInterceptor?
import okhttp3.Interceptor
import okhttp3.Response
class ErrorInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain?): Response {
val originalResponse = chain!!.proceed(chain.request())
if (shouldLogout(originalResponse)) {
// your logout logic here
// send empty response down the chain
return Response.Builder().build()
}
return originalResponse
}
private fun shouldLogout(response: Response) : Boolean {
if (response.isSuccessful) {
return false
}
// 401 and auth token means that we need to logout
return (response.code() == 401 &&
!response.headers().names().contains(AUTH_HEADER_KEY))
}
}
I have an OkHttp Interceptor which should ask for a token when the request is getting a 401 HTTP error. Now, the request to login service is done but then the chain is broken and the original request is not retried.
Here is the intercept method of my interceptor:
override fun intercept(chain: Interceptor.Chain): Response {
logger.d("AuthenticationServiceHolder $authenticationServiceHolder")
val originalRequest = chain.request()
logger.d("Intercepting call to ${originalRequest.method()} ${originalRequest.url()}")
val response: Response = chain.proceed(originalRequest)
val successful = response.isSuccessful
val code = response.code()
logger.d("Response successful: $successful - code: $code")
if (!successful && code == HttpURLConnection.HTTP_UNAUTHORIZED) {
logger.d("Token is $token")
val deviceUuid = deviceIdentificationManager.deviceUuid().blockingGet()
logger.d("Device uuid $deviceUuid")
if (deviceUuid != null) {
val authenticationService = authenticationServiceHolder.get()
if (authenticationService != null) {
token = reLogin(authenticationService, deviceUuid)
if (token != null) {
val headersBuilder = originalRequest.headers().newBuilder()
headersBuilder.removeAll(AUTHORIZATION_HEADER)
headersBuilder.add(AUTHORIZATION_HEADER, token!!)
val requestBuilder = originalRequest.newBuilder()
val request = requestBuilder.headers(headersBuilder.build()).build()
return chain.proceed(request)
} else {
logger.e("Token was not retrieved")
}
} else {
logger.e("Authentication service is null!")
}
}
}
return response
}
The reLogin() method is:
private fun reLogin(authenticationService: AuthenticationService, deviceUuid: UUID): String? {
logger.d("reLogin() - authenticationService $authenticationService")
val blockingGet = authenticationService?.login(LoginRequest(deviceUuid, clock.currentTime()))?.blockingGet()
logger.d("reLogin() - response $blockingGet")
val response = blockingGet ?: return null
logger.d("reLogin() - token ${response.token}")
return response.token
}
NEW:
As Mitesh Machhoya says, I've tried with 2 different instances of retrofit, one has the okhttp client with the interceptor and the another doesn't have it.
And now the login call is not intercepted but the execution of the Interceptor is broken, I mean the log trace of this class is:
- AuthenticationServiceHolder XXXmypackageXXX.AuthenticationServiceHolder...
- Intercepting call to GET XXXmyInterceptedCallXXX
- Response successful: false - code: 401
- Token is null
- Device uuid XXX
- reLogin() - authenticationService retrofit2.Retrofit$1#a5c0a25
And nothing more. I mean reLogin() - response..... is not printed. I'm sure that the login call is working because I see the login response in okhttp log.
Make reLogin request with other httpClient without attaching interceptor, then it will work well as you expected.
if you make reLogin request with same httpClient then it will gone through the interceptor and it override request everytime so try to make request using another httpClient
The code pasted is working well, the Login request is working but the response of login was changed on server side and the deserialisation made it crash and it broke the chain.
I am using OkHttp in my android application with several async requests. All requests require a token to be sent with the header. Sometimes I need to refresh the token using a RefreshToken, so I decided to use OkHttp's Authenticator class.
What will happen when 2 or more async requests get a 401 response code from the server at the same time? Would the Authenticator's authenticate() method be called for each request, or it will only called once for the first request that got a 401?
#Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{
return null;
}
How to refresh token only once?
Use a singleton Authenticator
Make sure the method you use to manipulate the token is Synchronized
Count the number of retries to prevent excessive numbers of refresh
token calls
Make sure the API calls to get a fresh token and the
local storage transactions to save the new token in your local stores are not asynchronous. Or if you want to make them asynchronous make sure you to you token related stuff after they are completed.
Check if the access token is refreshed by another thread already to
avoid requesting a new access token from back-end
Here is a sample in Kotlin
#SingleTon
class TokenAuthenticator #Inject constructor(
private val tokenRepository: TokenRepository
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
return if (isRequestRequiresAuth(response)) {
val request = response.request()
authenticateRequestUsingFreshAccessToken(request, retryCount(request) + 1)
} else {
null
}
}
private fun retryCount(request: Request): Int =
request.header("RetryCount")?.toInt() ?: 0
#Synchronized
private fun authenticateRequestUsingFreshAccessToken(
request: Request,
retryCount: Int
): Request? {
if (retryCount > 2) return null
tokenRepository.getAccessToken()?.let { lastSavedAccessToken ->
val accessTokenOfRequest = request.header("Authorization") // Some string manipulation needed here to get the token if you have a Bearer token
if (accessTokenOfRequest != lastSavedAccessToken) {
return getNewRequest(request, retryCount, lastSavedAccessToken)
}
}
tokenRepository.getFreshAccessToken()?.let { freshAccessToken ->
return getNewRequest(request, retryCount, freshAccessToken)
}
return null
}
private fun getNewRequest(request: Request, retryCount: Int, accessToken: String): Request {
return request.newBuilder()
.header("Authorization", "Bearer " + accessToken)
.header("RetryCount", "$retryCount")
.build()
}
private fun isRequestRequiresAuth(response: Response): Boolean {
val header = response.request().header("Authorization")
return header != null && header.startsWith("Bearer ")
}
}
I see here two scenarios based on how API which you call works.
First one is definitely easier to handle - calling new credentials (e.g. access token) doesn't expire old one. To achieve it you can add an extra flag to your credentials to say that credentials are being refreshed. When you got 401 response, you set flag to true, make a request to get new credentials and you save them only if flag equals true so only first response will be handled and rest of them will be ignored. Make sure that your access to flag is synchronized.
Another scenario is a little bit more tricky - every time when you call new credentials old one are set to be expired by server side. To handle it you I would introduce new object to be used as a semafore - it would be blocked every time when 'credentials are being refreshed'. To make sure that you'll make only one 'refresh credentials' call, you need to call it in block of code which is synchronized with flag. It can look like it:
synchronized(stateObject) {
if(!stateObject.isBeingRefreshed) return;
Response response = client.execute(request);
apiClient.setCredentials(response.getNewCredentials());
stateObject.isBeingRefreshed = false;
}
As you've noticed there is an extra check if(!stateObject.isBeingRefreshed) return; to cancel requesting new credentials by following requests which received 401 response.
In my case I implemented the Authenticator using the Singleton pattern. You can made synchronized that method authenticate. In his implementation, I check if the token from the request (getting the Request object from Response object received in the params of authenticate method) is the same that the saved in the device (I save the token in a SharedPreferences object).
If the token is the same, that means that it has not been refresed yet, so I execute the token refresh and the current request again.
If the token is not the same, that means that it has been refreshed before, so I execute the request again but using the token saved in the device.
If you need more help, please tell me and I will put some code here.
This is my solution to make sure to refresh token only once in a multi-threading case, using okhttp3.Authenticator:
class Reauthenticator : Authenticator {
override fun authenticate(route: Route?, response: Response?): Request? {
if (response == null) return null
val originalRequest = response.request()
if (originalRequest.header("Authorization") != null) return null // Already failed to authenticate
if (!isTokenValid()) { // Check if token is saved locally
synchronized(this) {
if (!isTokenValid()) { // Double check if another thread already saved a token locally
val jwt = retrieveToken() // HTTP call to get token
saveToken(jwt)
}
}
}
return originalRequest.newBuilder()
.header("Authorization", getToken())
.build()
}
}
You can even write a unit test for this case, too! 🎉
Add synchronized to authenticate() method signature.
And make sure getToken() method is blocking.
#Nullable
#Override
public synchronized Request authenticate(Route route, Response response) {
String newAccessToken = getToken();
return response.request().newBuilder()
.header("Authorization", "Bearer " + newAccessToken)
.build();
}
Make sure to use singleton custom Authenticator
When refreshing token successful return request with new token else return null.
class TokenAuthenticator(
private val sharedPref: SharedPref,
private val tokenRefreshApi: TokenRefreshApi
) : Authenticator,
SafeApiCall {
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
when (val tokenResponse = getUpdatedToken()) {
is Resource.Success -> {
val token = tokenResponse.data.token
sharedPref.saveToken(token)
response.request.newBuilder().header("Authorization", "Bearer $token").build()
}
else -> {
null
}
}
}
}
private suspend fun getUpdatedToken(): Resource<LoginResponse> {
return safeApiCall { tokenRefreshApi.refreshToken("Bearer ${sharedPref.getToken()}") }
}
}