2
votes

This approach always worked when updating a token. That is, with each request if I received an error 401, the operator retryWhen() triggered it updated the token.

Here is the code:

private Observable<TokenModel> refreshAccessToken() {
    Map<String, String> requestBody = new HashMap<>();
    requestBody.put(Constants.EMAIL_KEY, Constants.API_EMAIL);
    requestBody.put(Constants.PASSWORD_KEY, Constants.API_PASSWORD);

    return RetrofitHelper.getApiService().getAccessToken(requestBody)
            .subscribeOn(Schedulers.io())
            .doOnNext((AccessToken refreshedToken) -> {
                PreferencesHelper.putAccessToken(mContext, refreshedToken);
            });
}

public Function<Observable<Throwable>, ObservableSource<?>> isUnauthorized (){
    return throwableObservable -> throwableObservable.flatMap((Function<Throwable, ObservableSource<?>>) (Throwable throwable) -> {
        if (throwable instanceof HttpException) {
            HttpException httpException = (HttpException) throwable;

            if (httpException.code() == 401) {
                return refreshAccessToken();
            }
        }
        return Observable.error(throwable);
    });
}

I call isUnauthorized() at the retryWhen() operator where I make a request to the server

class RetrofitHelper {

    static ApiService getApiService() {
        return initApi();
    }

    private static OkHttpClient createOkHttpClient() {
        final OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        httpClient.addInterceptor(chain -> {
            Request originalRequest = chain.request();

            AccessToken accessToken= PreferencesHelper.getAccessToken(BaseApplication.getInstance());
            String accessTokenStr = accessToken.getAccessToken();
            Request.Builder builder =
                    originalRequest.newBuilder().header("Authorization", "Bearer " + accessTokenStr);

            Request newRequest = builder.build();
            return chain.proceed(newRequest);
        });

        return httpClient.build();
    }

    private static ApiService initApi(){
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Constants._api_url)
                .addConverterFactory(GsonConverterFactory.create())
                .addConverterFactory(ScalarsConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(createOkHttpClient())
                .build();
        return retrofit.create(ApiService.class);
    }
}

But we recently added Basic Auth, and now at the first request I get 401 and retryWhen() tries to update the Token, but still gets 401. That is, the doOnNext() does not work, but immediately the onError() works

private static Observable<AccessToken> refreshAccessToken() {
    return RetrofitHelper.getApiService()
            .getAccessToken(
                    Credentials.basic(
                            Constants._API_USERNAME, Constants._API_PASSWORD
                    ),
                    Constants._API_BODY_USERNAME,
                    Constants._API_BODY_PASSWORD,
                    Constants._API_BODY_GRANT_TYPE
            )
            .doOnNext((AccessToken refreshedToken) -> {
                PreferencesHelper.putObject(BaseApplication.getInstance(), PreferenceKey.ACCESS_TOKEN_KEY, refreshedToken);
                }

            });
}

// Api Service

public interface ApiService {
    // Get Bearer Token
    @FormUrlEncoded
    @POST("oauth/token")
    Observable<AccessToken> getAccessToken(@Header("Authorization") String basicAuth,
                                           @Field("username") String username,
                                           @Field("password") String password,
                                           @Field("grant_type") String grantType);
}

Here, tell me why this is a mistake? Why at the first request I get 401, and from the second request everything works?

2
if you are using using retrofit2 you need to remove / from end of your base url and add it before your post @POST("/oauth/token") - karan
@KaranMer, but why? - No Name

2 Answers

1
votes

I want to suggest a better solution.

public class RefreshTokenTransformer<T extends Response<?>> implements ObservableTransformer<T, T> {

    private class HttpCode {
        private static final int UNAUTHORIZED_HTTP_CODE = 401;
    }

    private ApiService mApiService;
    private UserRepository mUserRepository;

    public RefreshTokenTransformer(ApiService service, UserRepository userRepository) {
        mApiService = service;
        mUserRepository = userRepository;
    }

    @Override
    public ObservableSource<T> apply(final Observable<T> stream) {
        return stream.flatMap(new Function<T, ObservableSource<T>>() {
            @Override
            public ObservableSource<T> apply(T response) throws Exception {
                if (response.code() == HttpCode.UNAUTHORIZED_HTTP_CODE) {
                    return mApiService.refreshToken(mUserRepository.getRefreshTokenHeaders())
                            .filter(new UnauthorizedPredicate<>(mUserRepository))
                            .flatMap(new Function<Response<TokenInfo>, ObservableSource<T>>() {
                                @Override
                                public ObservableSource<T> apply(Response<TokenInfo> tokenResponse) throws Exception {
                                    return stream.filter(new UnauthorizedPredicate<T>(mUserRepository));
                                }
                            });
                }

                return stream;
            }
        });
    }

    private class UnauthorizedPredicate<R extends Response<?>> implements Predicate<R> {

        private UserRepository mUserRepository;

        private UnauthorizedPredicate(UserRepository userRepository) {
            mUserRepository = userRepository;
        }

        @Override
        public boolean test(R response) throws Exception {
            if (response.code() == HttpCode.UNAUTHORIZED_HTTP_CODE) {
                throw new SessionExpiredException();
            }

            if (response.body() == null) {
                throw new HttpException(response);
            }

            Class<?> responseBodyClass = response.body().getClass();
            if (responseBodyClass.isAssignableFrom(TokenInfo.class)) {
                try {
                    mUserRepository.validateUserAccess((TokenInfo) response.body());
                } catch (UnverifiedAccessException error) {
                    throw new SessionExpiredException(error);
                }
            }

            return true;
        }
    }
}

I`ve written the custom operator, which makes next actions:

  1. first request started, and we get 401 response code;

  2. then we execute /refresh_token request to update the token;

  3. after that if the token is refreshed successfully, we repeat the first request. if /refresh_token token is failed, we throw exception

Then, you can easy implement it in the any request like that:

 Observable
    .compose(new RefreshTokenResponseTransformer<Response<{$your_expected_result}>>
(mApiService, mUserRepository()));

One more important thing: Most likely, that your initial observable for retrofit has params, like that:

mApiService.someRequest(token)

if the param is expected to change during the performing RefreshTokenTransformer(e.g. /refresh_token request will get new access token and you save it somewhere, then you want to use a fresh access token to repeat the request) you will need to wrap your observable with defer operator to force the creating of new observable like that:

Observable.defer(new Callable<ObservableSource<Response<? extends $your_expected_result>>>() {
            @Override
            public Response<? extends $your_expected_result> call() throws Exception {
                return mApiService.someRequest(token);
            }
        })
0
votes

I think it does not need to use interceptor instead you implement Authenticator by which you can access refreshed token and okhttp automatically will handle that. if you get 401 it updates header with refreshed token and make new request.

  public class TokenAuthenticator implements Authenticator {
@Override
  public Request authenticate(Proxy proxy,     Response response) throws IOException {
      // Refresh your access_token using a    synchronous api request
    newAccessToken = service.refreshToken();

          // Add new header to rejected request and retry   it
          return response.request().newBuilder()
            .header(AUTHORIZATION,    newAccessToken)
            .build();
}