OkHttp requests are leaking when refreshing server tokens through interceptors - android

I use RxJava and Retrofit to execute OkHttp requests. I can see in Android Studio's network profiler that my requests are leaked, because they are keeping the AsyncTask threads alive. No response for them and their size is null. I can see the original request, the token request, the updated request in the Thread View in network profiler. But the original request never finishes.
In extreme cases, like in QA environment where the tokens are refreshed every minute for some reason the threadpool becomes full and no more calls can be made. I think we can call chain.proceed as many times as we want, but I think the error is still somewhere there. Here is the interceptor code:
private static okhttp3.Interceptor oauthInterceptor = chain -> {
OAuthToken token = OAuthToken.getOAuthToken();
long initialTokenCreated = token.getCreatedUtc();
Request request = changeTokenInRequest(chain.request(), token);
Response response = chain.proceed(request);
boolean forceTokenRefresh = false;
if (initialTokenCreated != token.getCreatedUtc()) {
// Then the token has been updated since we started this request
request = changeTokenInRequest(chain.request(), token);
response = chain.proceed(request);
}
String jsonType = "application/json";
if (!response.body().contentType().toString().contains(jsonType)) {
forceTokenRefresh = true;
}
// 401: Forbidden, 403: Permission denied
if (forceTokenRefresh || ((response.code() == 401 || response.code() == 403)
&& OAuthToken.getOAuthToken().getRefreshToken() != null)) {
OAuthToken refreshedToken = refreshToken();
if (refreshedToken == null) {
// Then there was a problem refreshing the token
return response;
}
// Recreate the request with the new access token
request = changeTokenInRequest(chain.request(), refreshedToken);
return chain.proceed(request);
} else {
return response;
}
};
protected static okhttp3.Request changeTokenInRequest(Request request, OAuthTokenBase token) {
Headers.Builder builder = request.headers().newBuilder().removeAll("Authorization");
if (!TextUtils.isEmpty(token.getTokenType()) && !TextUtils.isEmpty(token.getAccessToken())) {
builder = builder.add("Authorization", token.getTokenType() + " " + token.getAccessToken());
}
request = request.newBuilder().headers(builder.build()).build();
return request;
}
That's how I make the calls:
public static Observable<ResultPaginatedReply<BasicUser>> getGroupsMembers(String type, String id, int page) {
return getMsApiService().getGroupsMembers(type, id, page)
.subscribeOn(Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR))
.observeOn(AndroidSchedulers.mainThread());
}

Turns out that if you don't use the Authenticator class in OkHttp like me here, than you have to manually close the response body when the token is refreshed. #DallinDyer's comment helped me here.
So I added
response.body().close();
after the error checks and voilĂ .

Related

What is the right way use refreshToken?

I am working with an API that needs for some request an access token. The token get refreshed every 10 minutes, so i need to use a refresh token request. The problem is that the refresh token request requires a valid token that didn't expired yet. To summarize my question, is it normal that the refresh token requires unexpired token or there is another safe way to change the logic of refreshing it?
P.S: In the android app usually to refresh token you need get the first failed request than you request an new one and if you you choose to work with WorkManager you'll need at least 15 minutes for the periodicWorkRequest.
In my case this work me as below:
httpClient.authenticator(new Authenticator() {
#Nullable
#Override
public Request authenticate(Route route, Response response) throws IOException {
String SavedAccessToken = "Bearer "+MyApplication.getLoginUser().getAccessToken();
String responseHeader = response.request().header("Authorization");
if (!SavedAccessToken.equals(responseHeader))
return null;
// request new key
String accessToken = null;
Call<ResponModel<User>> call = RetrofitManager.getRetrofitManager().getApiService().requestAccessToken(MyApplication.getLoginUser());
try {
retrofit2.Response responseCall = call.execute();
ResponModel<User> responseRequest = (ResponModel<User>) responseCall.body();
if (responseRequest != null) {
accessToken = responseRequest.getData().getAccessToken();
User user = new User();
user.setUsername(PrefsUtils.get("UserName","").toString());
user.setAccessToken(accessToken);
user.setRefreshToken(responseRequest.getData().getRefreshToken());
MyApplication.updateUser(user);
}
} catch (Exception ex) {
Log.d("ERROR", "onResponse: " + ex.toString());
}
if (accessToken != null)
// retry the failed 401 request with new access token
return response.request().newBuilder()
.header("Authorization", "Bearer "+ accessToken) // use the new access token
.build();
else
return null;
}
});

Achieving a global force logout using Okhttp's authenticator (along with RetroFit , RxJava and Dagger2)

The Task
I have a situation in which if a refresh token request comes back with a 401, I would force a logout by clearing the user from the room and from memory. I would want to achieve this from inside of the Authenticator OR the Interceptor so I don't have to check for 401 as a response to each call.
The Authenticator
public Request authenticate(Route route, Response response) throws IOException {
// if (responseCount(response) >= 3) {
// return null;
// }
//
UserCredentials userCredentials = new
UserCredentials().
CreateRefreshTokenRequest("refresh_token",
requestHeaders.getAccessToken().getRefreshToken(),
oAuthClient.getClientId(),
oAuthClient.getClientSecret());
Request.Builder builder = response.request().newBuilder();
if (accountsServicesHolder.getAccountsServices() == null)
return null;
//this HAS TO BE a Synchronous process. The usual RXjava shenanigans can't be applied here.
try {
//Synchronous call to get new token
AccessToken accessToken = accountsServicesHolder.
getAccountsServices().
refreshToken(userCredentials.toMap()).
blockingGet();
//persist the token in the db
//userDao.save()
//persist the token in memory
//send an updated version to the backend
builder.header("Authorization", "Bearer " + accessToken.getAccessToken());
} catch (Exception ex) {
if (ex instanceof HttpException) {
return builder.
url(linksProvider.getProductionUrl().concat(logoutEndpoint)).
build(); //Redirecting this authenticator to a logout ws so that we do not get a 401 . Something goes wrong here
}
}
return builder.build();
}
As you can see, if we do not get a token on basic of the refresh token, I am cunningly inserting a new URL into the builder. This is so that I do not get a 401 being bubbled up to where I made the initial call. I do not want a user to get a "401 You have not authorized" message rather I would display a toast saying "Session Expired"
The Request Interceptor
#Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request.Builder builder = original.newBuilder().
header("Authorization", "Bearer " + requestHeaders.getAccessToken().getAccessToken()).
header("Accept-Language", requestHeaders.getLanguage()).
method(original.method(), original.body());
Request newRequest = builder.build();
Response response = chain.proceed(newRequest);
if (response.code() == 200
&& response.request().url().toString().contains("logout")) {
forceUserOut();
}
return response;
}
The forceUserOut method Broadcasts to the system that a logout has happened.
Since we have protected against a 401 now no errors would be displayed to the user.
The Problem
Okay, now we come to the problem. The place where I thought I was being cunned is perhaps not so smart after all. The authenticator runs an extra time after the logout API is called which runs after a thing that is supposed to happen afterward twice.
As far as I understand, the authenticator should have just executed the logout service and the interceptor should have started doing its job then.
The authenticator, however, does the following
1) Tries to get the new access token
2) Fails and calls the logout API (invokes the Interceptor and broadcasts the logout)
3) Fails and calls the logout API (invokes the interceptor and broadcasts the logout)
4) End
I know I might be breaking something inside the OkHttps authentication mechanism, but what other methods could I use in order to achieve a forced logout in a way I do not have to write a 401 check on all the APIs?
Extremely apologetic for the long question.

Put all ongoing requests to queue retrofit and okhttp

I'm working with the authenticator of OKHttp that will retry to get new access token if we got 401 status error, but my app have to call many APIs in the same time, resulting in corrupted data, because existed refresh token will be removed when request - but the other API caller still depend on this token to use. So my question : is there anyway to put the request in queue (or at least cancel) all other api request when we got 401 error status code?
This is my authenticator:
public Request authenticate(Route route, Response response) throws IOException {
// Refresh your access_token using a synchronous api request
access_token = getNewAccessTokenHere();
// Add new header to rejected request and retry it
return response.request().newBuilder()
.header("Authorization", "Bearer " + access_token)
.build();
} else {
ToastUtil.toast("login again");
return null;
}
}
My goal is let other api waiting for the response of first request and use the new access_token.
I know this question is quite old, but I found myself having the same problem lately and I came by a solution which has worked for me and I believe it could help someone else in the same situation.
The solution to attach a Dispatcher to the OkHttp client and limit the amount of max requests does not seem to work on retrofit out of the box, as stated by Jake Wharthon .
So my solution was to synchronize the authenticate method on my custom Authenticator, making concurrent calls, to my singleton Retrofit instance, wait until the authenticate routine is finished for each thread. This way, the first call with an unauthorized response can refresh the token and inform to the next calls, which also got an unauthorized response, that a new access token is already available.
public class MyAuthenticator implements Authenticator {
private boolean isRefreshed = false;
// Call when a new request is being made. Concurrent request should call this method to enable the refresh routine
public void newRequest(){
isRefreshed = false;
}
#Nullable
#Override
public synchronized Request authenticate(#NonNull Route route, #NonNull Response response) throws IOException { // Synchronize the method to avoid refreshing thread overlapping
if (responseCount(response) > 3) {
return null;
}
if (!isRefreshed){
// Refresh your access_token using a synchronous api request
String accessToken = getNewAccessTokenHere();
// Saves the new access token
saveNewAccessToken(accessToken);
isRefreshed = true;
// Add new header to rejected request and retry it
return response.request().newBuilder()
.removeHeader("Authorization") // removes the old header, avoiding duplications
.addHeader("Authorization", "Bearer " + accessToken)
.build();
}
else{ // Access token already refreshed so retry with the new one
// Get the saved access token
String accessToken = getAccessToken();
return response.request()
.newBuilder()
.removeHeader("Authorization")
.addHeader("Authorization", accessToken)
.build();
}
}
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
}
You can use the Dispatcher to access all in-flight calls and cancel them.
https://square.github.io/okhttp/3.x/okhttp/okhttp3/Dispatcher.html

How to refresh Cookies using okhttp3?

The REST API I connect to uses cookie-based authorization. Basically, issuing a call, I get a cookie, which then I use in all my requests. That cookie has an expiry time on the Server side. Once cookie expired, I get notified, I have to acquire a new cookie to proceed.
To achieve this, I am using OkHttp3 client, to which I attached a PersistentCookieJar. Somewhere in between, I also have an Interceptor that catches the session expired responses, and tries to renew the cookie. After my Interceptor does the call to renew the cookie, it re-tries the previously failed call. Which, fails again. I suppose it is because the retry of the call is done with old cookie.
Any suggestions?
Some code snippets below:
//OkHttp client with the interceptor and the cookiejar
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(sessionExpiryInterceptor);
if (cache != null) {
builder.cache(cache);
}
return builder
.cookieJar(cookieJar)
.connectTimeout(1, TimeUnit.MINUTES)
.build();
The PersistentCookieJar is used as described here: https://github.com/franmontiel/PersistentCookieJar
And last, the Interceptor:
public class SessionExpiryInterceptor implements Interceptor {
#Override public Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
final Response response = chain.proceed(request);
//analyze to see if it is "session expired"
String bodyString = response.body().string();
Response copyResponse = copyResponse(response, bodyString);
//attempt to parse the original body
if (isRefreshRequired(bodyString)) {
//we send the response copy from here on out, as the original no longer as a "body" - extracted and parsed earlier
return refreshData(request, copyResponse, chain);
} else {
//return the copy since body was extracted earlier
return copyResponse;
}
}
private boolean isRefreshRequired(String responseBody) throws IOException {
//does some parsing of the responseBody to extract a "status" and "message"
return status == RESPONSE_CODE_NO_SESSION || message.contains("session");
}
private Response refreshData(Request request, Response responseCopy, Chain chain) throws IOException {
//does some checks before attempting to actually refresh the cookie,
mAuthService.login(user, pass)
.subscribeOn(Schedulers.immediate())
.observeOn(Schedulers.immediate())
.subscribe(createLoginSubscriber());
if (mDataRefreshed) {
// rebuild the request with the new acquired cookie
Request retry = createRetryRequest(request);
return chain.proceed(retry);// !!! this call fails again - I get "session expired" response message.
} else {
//for whatever reason the refresh failed - send back the original response's copy
return responseCopy;
}
}
private Subscriber<LoginResponse> createLoginSubscriber() {
return new Subscriber<LoginResponse>() {
#Override public void onCompleted() {
}
#Override public void onError(Throwable e) {
LOG.e(e, "login failed" + e.getMessage());
}
#Override public void onNext(LoginResponse response) {
if(response.isSuccess()) {
mDataRefreshed = true;
} else {
mDataRefreshed = false;
//some cleanup code here
}
}
};
}
private Request createRetryRequest(Request source) {
//I suspect this copy might be the issue?
return source.newBuilder().build();
}
//copies the given response rewriting the body with the given one with character set UTF-8
private Response copyResponse(Response original, String body) {
return original.newBuilder()
.body(new RealResponseBody(original.headers(), new Buffer().writeUtf8(body)))
.build();
}
}

How to handle auth0 403 error without adding specific code everywhere (Retrofit/okhttp/RxAndroid)

I am using Auth0, which gives me a JWT (json web token) and a refreshtoken. I use this JWT in the http headers to communicate with my backend.
It could happen, that the server gives me a 403, when it decides that the JWT has expired. In this event, I can ask Auth0 to issue me a new JWT, using the refreshtoken. It means I call the Auth0 backend, pass it the refreshtoken, and it gives me a new JWT, which I can then use in my requests.
My question is, how can I efficiently write this behaviour in all my networking code? I will have a couple of endpoints to talk to, and they all might return the 403.
I am thinking I should first make an interceptor that adds the JWT to all requests.
Then there should be behaviour that detects the 403, quietly does a networkcall to Auth0, retrieving the new JWT. Then the original request should be tried again, with the new JWT in its headers.
So I would prefer to have this 403 handling somewhere invisible to my other code, and definitely not have to rewrite it everywhere.
Any pointers on how to achieve this will be appreciated.
--
To be clear, I am basically looking for pointers on how to achieve this using RxAndroid Observables. When a certain Observable finds the 403, it should 'inject' a new network call.
I solved this issue by writing an Interceptor for OkHttp. It checks the statuscode of the network call. If it's a 403, call Auth0 servers and request a new id_token. Then use this token in a new version of the original request.
To test, I wrote a little webserver that checks the TestHeader for fail or succeed and returns a 403 if it's fail.
public class AuthenticationInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request authenticationRequest = originalRequest.newBuilder()
.header("TestHeader", "fail")
.build();
Response origResponse = chain.proceed(authenticationRequest);
// server should give us a 403, since the header contains 'fail'
if (origResponse.code() == 403) {
String refreshToken = "abcd"; // you got this from Auth0 when logging in
// start a new synchronous network call to Auth0
String newIdToken = fetchNewIdTokenFromAuth0(refreshToken);
// make a new request with the new id token
Request newAuthenticationRequest = originalRequest.newBuilder()
.header("TestHeader", "succeed")
.build();
// try again
Response newResponse = chain.proceed(newAuthenticationRequest);
// hopefully we now have a status of 200
return newResponse;
} else {
return origResponse;
}
}
}
Then I attach this Interceptor to an OkHttpClient which I plug into the Retrofit adapter:
// add the interceptor to an OkHttpClient
public static OkHttpClient getAuthenticatingHttpClient() {
if (sAuthenticatingHttpClient == null) {
sAuthenticatingHttpClient = new OkHttpClient();
sAuthenticatingHttpClient.interceptors().add(new AuthenticationInterceptor());
}
return sAuthenticatingHttpClient;
}
// use the OkHttpClient in a Retrofit adapter
mTestRestAdapter = new RestAdapter.Builder()
.setClient(new OkClient(Network.getAuthenticatingHttpClient()))
.setEndpoint("http://ip_of_server:port")
.setLogLevel(RestAdapter.LogLevel.FULL)
.build();
// call the Retrofit method on buttonclick
ViewObservable.clicks(testNetworkButton)
.map(new Func1<OnClickEvent, Object>() {
#Override
public Object call(OnClickEvent onClickEvent) {
return mTestRestAdapter.fetchTestResponse();
}
}
)
Instead of refreshing tokens only after receiving a 403 response, you could check the expiration time locally and refresh accordingly by checking the token's exp claim. For example, this example uses the same approach in Angular. It's not specific to Android, but the idea is the same:
jwtInterceptorProvider.tokenGetter = function(store, jwtHelper, auth) {
var idToken = store.get('token');
var refreshToken = store.get('refreshToken');
if (!idToken || !refreshToken) {
return null;
}
// If token has expired, refresh it and return the new token
if (jwtHelper.isTokenExpired(idToken)) {
return auth.refreshIdToken(refreshToken).then(function(idToken) {
store.set('token', idToken);
return idToken;
});
// If not expired, return the token directly
} else {
return idToken;
}
}

Categories

Resources