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()}") }
}
}
Related
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
How can I handle 'refresh token' when 'access_token' is expired?
I know how it works. But what I want to know is implementing once and apply it to all the APIs.
When access token is expired, all the APIs are blocked(401) and need to request new token with refresh token.
So, I tried to do it within 'intercepter' because it can handle the request and response before sending or before handling in the application.
The process is like this.
request an API
catch the response
if it's 401, call refresh token API
get the response and request the original API that I was going to call.
get the proper response from the original API.
// intercepter
val originalRequest = it.request()
val newRequestBuilder = originalRequest.newBuilder()
val response = it.proceed(newRequestBuilder.build())
if (response.code == 401) {
// TODO: refresh token and request again and get the original response
}
response
Refresh tokens without getting "Error" response from API (Write only once)
I would suggest you to use Authenticator. OkHttp will automatically ask the Authenticator for credentials when a response is 401 Not Authorized retrying last failed request with them.
Create a class MyAuthenticator and add the following code:
class MyAuthenticator: Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
// set maixmum retry count
if (response.responseCount >= 3) {
return null // If we've failed 3 times, give up.
}
// write code to refresh the token
val call = MyRetrofitClient.MyApi().refreshAccessToken()
val res = call.execute()
if (res.isSuccessful){
val newAccessToken = res.body // your new token from response
//
response.request
.newBuilder()
.header("bearerToken", newAccessToken)
.build()
}else{
return null
}
return null
}
//
private val Response.responseCount: Int
get() = generateSequence(this) { it.priorResponse }.count()
}
Now you can attach this Authenticator to your OkHttpClient the same way you do with Interceptors
private val client= OkHttpClient.Builder()
.addInterceptor(MyInterceptor())
.authenticator(MyAuthenticator()) // authenticator we created
.build()
Finally add this client to the Retrofit Builder:
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client) // from 2nd step
.build()
That's all, Now if 401 error occur, Authenticator will be called automatically and token will be refreshed and the pending API will be continued without getting error response.
In my project, there are network requests that require authentication via bearer tokens and some requests that don't. Is there a way to specify the urls that do not need bearer tokens? If I just add the Auth plugin, then I always get a 401 response for the network calls that don't require bearer tokens.
This is my implementation right now:
interface MyService {
...
companion object Factory {
fun build(getToken: GetToken, login: Login, saveToken: SaveToken): MyService {
return MyServiceImpl(httpClient = HttpClient(CIO) {
install(JsonFeature) {
serializer = KotlinxSerializer(
kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
}
)
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(Auth) {
lateinit var tokenInfo: TokenInfo
bearer {
refreshTokens {
getRefreshedTokens(
tokenInfo = tokenInfo,
login = login,
saveToken = saveToken
)
}
loadTokens {
tokenInfo = getToken.execute().firstOrNull() ?: TokenInfo(
Constants.EMPTY_STRING, Constants.EMPTY_STRING
)
BearerTokens(
accessToken = tokenInfo.accessToken,
refreshToken = tokenInfo.refreshToken
)
}
}
}
})
}
...
}
}
You can filter requests by some condition that should include the Authorization header initially. For each other request, the second request with the Authorization header will be made if a server replied with 401 Unauthorized status. Here is an example configuration:
val client = HttpClient {
install(Auth) {
bearer {
sendWithoutRequest { request -> request.url.encodedPath.startsWith("/restricted") }
// ...
}
}
}
A bearer token is nothing but a header Authorization: Bearer XXX - If you're looking for a tech specific way to handle this (drop in some module, etc) then you may be out-of-luck
However, this problem occurs frequently enough in my life - the solution is to actually dynamically manipulate the headers - Basically, use the strategy pattern. If you can create instances of your client, then each instance would get its own strategy. If you can't, then install a function that gets called with every request to determine the header based on whatever logic you need.
In your case, a custom:
install(Auth) {
// your code here
}
I'm trying to implement a refresh token process. Basically i have an access token that expires in an hour, and a refresh token which i send to the server in order to get a new access token.
I'm using okhttp as my http client and it has built in support for authentication, but the problem is that in the authenticate method I have to return a Request object with the new access token inside.
To do this, first i must wait for an api call to retrieve the new access token using the refresh token that i provide, and as you know, this process takes a while to complete.
My question is how can I wait for the result to come, and then return my Request object?
class TokenAuthenticator : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
//With RX
val newToken = getTokenFromServer()?.doOnSuccess {
return response.request().newBuilder().url("URL").addHeader("token", it.result?.token)
.build()
}
//First coroutine attempt
val job = runblocking { launch { getTokenFromServer() } }
job.join()
//... create request
return request
//Second coroutine attempt
val request = runBlocking {
val newToken = getTokenFromServer()
//... create new request object here using newToken
return#runBlocking request
}
return request
}
fun getTokenFromServer(): Single<ApiResponse>? {
//... gets data from server and returns the result
return newToken
}
}
side notes:
When using RX, I can't return a request object from inside the doOnSuccess.
In first coroutine attempt, using job.join() requires the authenticate method to be a suspend function, and I am not allowed to change it.
In second coroutine attempt it seems to be ok but doesn't it block the UI thread doing it this way? or since the whole process is happening outside of the UI thread the app won't freeze!?
Thank you.
As it turns out, since we are already on a background thread, we can use synchronous tasks and wait for the response.
override fun authenticate(route: Route?, response: Response): Request? {
//With RX
val newToken = getTokenFromServer().blockingGet()
return newToken
//Or a retrofit call object
val newToken = getTokenFromServer().execute()
return newToken
}
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))
}
}