OkHttp Interceptor chain broken when getting token - android

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.

Related

How can I refresh jwt and request the original request again and get the response?

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.

Twitter oauth/request_token 200 code with empty response body

I'm implementing Twitter OAuth flows as per:
https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter
I am getting a response back for the first step (oauth/request_token) which has a 200 code, but the response body is completely empty.
I'm using Retrofit to call the API, and have hooked up an interceptor OkHttpClient to debug the response like so:
val client = OkHttpClient.Builder().also { builder ->
builder.addInterceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
response
}
}.build()
Then setting up Retrofit like so:
Retrofit.Builder()
.baseUrl(TWITTER_AUTH_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
.create(TwitterAuthRetrofit::class.java)
.getRequestToken(
authorizationHeaders
).enqueue(object : Callback<TwitterRequestToken> {
override fun onResponse(call: Call<TwitterRequestToken>, response: Response<TwitterRequestToken>) {
onSuccess(response.body())
}
override fun onFailure(call: Call<TwitterRequestToken>, t: Throwable) {
onFailure()
}
})
When I debug in the interceptor, I can see the response is successful (200) but the response body is empty, which I think is causing my Gson deserialization to fail.
The result of calling response.body.contentLength() in the interceptor is -1.
The result of calling response.code in the interceptor is 200.
Here is the model I am attempting to deserialize the response body to:
data class TwitterRequestToken(
#SerializedName(value = "oauth_token")
val token: String,
#SerializedName(value = "oauth_token_secret")
val tokenSecret: String,
#SerializedName(value = "oauth_callback_confirmed")
val callbackConfirmed: Boolean
)
Note I am using #SerializedName to provide the keys for the response body, whilst the names of my properties are arbitrary to our app (we use camel case). I add a GsonConverterFactory to the Retrofit instance using the builder and have done this in the same way for many other requests before with no issues.
Here is the response I am getting from the API, which I am looking at via debugging in the interceptor above:
Response{protocol=h2, code=200, message=, url=https://api.twitter.com/oauth/request_token}
And here is the cause message from the Throwable I am getting in the onFailure callback from Retrofit:
com.google.gson.stream.MalformedJsonException:
Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
Has anyone got any idea what might cause this?
Finally figured it out, hope this helps someone in future...
The response body from the Twitter API for oauth/request_token isn't encoded as JSON; you will need to read it from the response buffer. Specifically, when implementing the API with Retrofit, you will want your Retrofit interface to return ResponseBody (rather than your custom class), remove GSON from the Retrofit builder and, in the onResponseCallback from Retrofit, write the following code to read the buffer to a string, then split the string on & to get each key val pair, then you can split each of these on = and make sure you have all 3 values before constructing your model:
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
response.body()?.also { body ->
body.source().readString(Charsets.UTF_8).split('&').map { param ->
param.split('=').let { keyVal ->
keyVal[0] to keyVal[1]
}
}.toMap().let { paramMap ->
val oauthToken = paramMap["oauth_token"]
val oauthTokenSecret = paramMap["oauth_token_secret"]
val oauthCallbackConfirmed = paramMap["oauth_callback_confirmed"]?.toBoolean()
if (oauthToken == null || oauthTokenSecret == null || oauthCallbackConfirmed == null) {
onFailure()
} else {
onSuccess(
TwitterRequestToken(
oauthToken,
oauthTokenSecret,
oauthCallbackConfirmed
)
)
}
}
} ?: onFailure()
}

How to differentiate between different Retrofit responses in Authenticators or Interceptors?

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))
}
}

Retrofit2: how to save cookies from response

I need to add some authorization information from cookie in response to next requests.
It works in postman - I make authorization request, then second request, which works fine. But if I delete cookies - second request returns error and I have to do authorization request again.
But in my application this second request always returns the same error. I tried to find needed cookie by using interceptor, but I hadn't found it
val client = OkHttpClient.Builder()
.addInterceptor(OAuthInterceptor())
private class OAuthInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val request = chain.request()
com.app.logic.toLog("${chain.proceed(request).header("set-cookie")} ") // it's not that cookie what I looking for
val headers = chain.proceed(request).headers()
headers.names().forEach {
val s = headers.get(it)
com.app.logic.toLog("$it -> $s")
}
return chain + (Session.authConsumer?.let { consumer ->
consumer.sign(request).unwrap() as Request
} ?: request)
}
}
Does anybody know what else could I try?
So, finally I've found solution for working with cookies
val client = OkHttpClient.Builder()
.cookieJar(UvCookieJar())
private class UvCookieJar : CookieJar {
private val cookies = mutableListOf<Cookie>()
override fun saveFromResponse(url: HttpUrl, cookieList: List<Cookie>) {
cookies.clear()
cookies.addAll(cookieList)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> =
cookies
}
You can use this this gist on how to intercept cookies when you receive them and send them back in your request in header.

Okhttp Authenticator multithreading

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()}") }
}
}

Categories

Resources