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
Related
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;
}
});
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.
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Ă .
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;
}
}
I am trying to automatically refresh an auth token if it is expired. I am using the new Interceptor class that was introduced in OkHttp 2.2. In the intercept method I am trying the original request with chain.proceed(request), checking the response code, and if the token is expired I am making a call to a separate Retrofit service, synchronously, to obtain a new token.
The strange thing is, no code past the synchronous call seems to run. If I try debugging with a breakpoint on the synchronous call's line, then do a step-over, I am stopped in Dispatcher.java at :
if (!executedCalls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
Any idea as to what I might be doing wrong here? I could probably just craft a new request by hand, but I am just kind of curious why a Retrofit call doesn't seem to work here.
My Interceptor:
public class ReAuthInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// try the request
Response response = chain.proceed(request);
// if we receive a "401 - Not Authorized" then refresh the auth token and try again
if (response.code() == 401) {
// get a new auth token and store it
UserToken userToken = MobileClient.getOkraService().login(AuthUtil.getUserToken(MSWorksApplication.getContext()));
AuthUtil.authenticate(MSWorksApplication.getContext(), userToken);
Log.d("TEST", "TEST TEST TEST");
// use the original request with the new auth token
Request newRequest = request.newBuilder().header("Authorization", AuthUtil.getAuthToken(MSWorksApplication.getContext())).build();
return chain.proceed(newRequest);
}
else {
// the auth token is still good
return response;
}
}
}
I had this problem - if you're getting an HTTP response code that would cause Retrofit to call failure() instead of success() an exception will be thrown.
If you wrap
UserToken userToken = MobileClient.getOkraService().login(AuthUtil.getUserToken(MSWorksApplication.getContext()));
in a try/catch(RetrofitError e) you'll be able to execute code after a failure, however I've found this to be quite cumbersome and am still in the process of finding a nicer solution.