Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Refreshing OAuth token using Retrofit without modifying all calls

We are using Retrofit in our Android app, to communicate with an OAuth2 secured server. Everything works great, we use the RequestInterceptor to include the access token with each call. However there will be times, when the access token will expire, and the token needs to be refreshed. When the token expires, the next call will return with an Unauthorized HTTP code, so that's easy to monitor. We could modify each Retrofit call the following way: In the failure callback, check for the error code, if it equals Unauthorized, refresh the OAuth token, then repeat the Retrofit call. However, for this, all calls should be modified, which is not an easily maintainable, and good solution. Is there a way to do this without modifying all Retrofit calls?

like image 569
Daniel Zolnai Avatar asked Oct 04 '22 09:10

Daniel Zolnai


4 Answers

Please do not use Interceptors to deal with authentication.

Currently, the best approach to handle authentication is to use the new Authenticator API, designed specifically for this purpose.

OkHttp will automatically ask the Authenticator for credentials when a response is 401 Not Authorised retrying last failed request with them.

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

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        // Null indicates no attempt to authenticate.
        return null;
    }

Attach an Authenticator to an OkHttpClient the same way you do with Interceptors

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);

Use this client when creating your Retrofit RestAdapter

RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(ENDPOINT)
                .setClient(new OkClient(okHttpClient))
                .build();
return restAdapter.create(API.class);
like image 73
lgvalle Avatar answered Oct 14 '22 05:10

lgvalle


If you are using Retrofit >= 1.9.0 then you could make use of OkHttp's new Interceptor, which was introduced in OkHttp 2.2.0. You would want to use an Application Interceptor, which permits you to retry and make multiple calls.

Your Interceptor could look something like this pseudocode:

public class CustomInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // try the request
        Response response = chain.proceed(request);

        if (response shows expired token) {
            // close previous response
            response.close()

            // get a new token (I use a synchronous Retrofit call)

            // create a new request and modify it accordingly using the new token
            Request newRequest = request.newBuilder()...build();

            // retry the request
            return chain.proceed(newRequest);
        }

        // otherwise just pass the original response on
        return response;
    }

}

After you define your Interceptor, create an OkHttpClient and add the interceptor as an Application Interceptor.

    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.interceptors().add(new CustomInterceptor());

And finally, use this OkHttpClient when creating your RestAdapter.

    RestService restService = new RestAdapter().Builder
            ...
            .setClient(new OkClient(okHttpClient))
            .create(RestService.class);

Warning: As Jesse Wilson (from Square) mentions here, this is a dangerous amount of power.

With that being said, I definitely think this is the best way to handle something like this now. If you have any questions please don't hesitate to ask in a comment.

like image 35
theblang Avatar answered Oct 14 '22 05:10

theblang


TokenAuthenticator depends an a service class. The service class depends on an OkHttpClient instance. To create an OkHttpClient I need the TokenAuthenticator. How can I break this cycle? Two different OkHttpClients? They are going to have different connection pools..

If you have, say, a Retrofit TokenService that you need inside your Authenticator but you would only like to set up one OkHttpClient you can use a TokenServiceHolder as a dependency for TokenAuthenticator. You would have to maintain a reference to it at the application (singleton) level. This is easy if you are using Dagger 2, otherwise just create class field inside your Application.

In TokenAuthenticator.java

public class TokenAuthenticator implements Authenticator {

    private final TokenServiceHolder tokenServiceHolder;

    public TokenAuthenticator(TokenServiceHolder tokenServiceHolder) {
        this.tokenServiceHolder = tokenServiceHolder;
    }

    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {

        //is there a TokenService?
        TokenService service = tokenServiceHolder.get();
        if (service == null) {
            //there is no way to answer the challenge
            //so return null according to Retrofit's convention
            return null;
        }

        // Refresh your access_token using a synchronous api request
        newAccessToken = service.refreshToken().execute();

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

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        // Null indicates no attempt to authenticate.
        return null;
    }

In TokenServiceHolder.java:

public class TokenServiceHolder {

    TokenService tokenService = null;

    @Nullable
    public TokenService get() {
        return tokenService;
    }

    public void set(TokenService tokenService) {
        this.tokenService = tokenService;
    }
}

Client setup:

//obtain instance of TokenServiceHolder from application or singleton-scoped component, then
TokenAuthenticator authenticator = new TokenAuthenticator(tokenServiceHolder);
OkHttpClient okHttpClient = new OkHttpClient();    
okHttpClient.setAuthenticator(tokenAuthenticator);

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .client(okHttpClient)
    .build();

TokenService tokenService = retrofit.create(TokenService.class);
tokenServiceHolder.set(tokenService);

If you are using Dagger 2 or a similar dependency injection framework there are some examples in the answers to this question

like image 45
David Rawson Avatar answered Oct 14 '22 06:10

David Rawson


Using TokenAuthenticator like @theblang answer is a correct way for handle refresh_token.

Here is my implement (I have using Kotlin, Dagger, RX but you may use this idea for implement to your case)
TokenAuthenticator

class TokenAuthenticator @Inject constructor(private val noneAuthAPI: PotoNoneAuthApi, private val accessTokenWrapper: AccessTokenWrapper) : Authenticator {

    override fun authenticate(route: Route, response: Response): Request? {
        val newAccessToken = noneAuthAPI.refreshToken(accessTokenWrapper.getAccessToken()!!.refreshToken).blockingGet()
        accessTokenWrapper.saveAccessToken(newAccessToken) // save new access_token for next called
        return response.request().newBuilder()
                .header("Authorization", newAccessToken.token) // just only need to override "Authorization" header, don't need to override all header since this new request is create base on old request
                .build()
    }
}

For prevent dependency cycle like @Brais Gabin comment, I create 2 interface like

interface PotoNoneAuthApi { // NONE authentication API
    @POST("/login")
    fun login(@Body request: LoginRequest): Single<AccessToken>

    @POST("refresh_token")
    @FormUrlEncoded
    fun refreshToken(@Field("refresh_token") refreshToken: String): Single<AccessToken>
}

and

interface PotoAuthApi { // Authentication API
    @GET("api/images")
    fun getImage(): Single<GetImageResponse>
}

AccessTokenWrapper class

class AccessTokenWrapper constructor(private val sharedPrefApi: SharedPrefApi) {
    private var accessToken: AccessToken? = null

    // get accessToken from cache or from SharePreference
    fun getAccessToken(): AccessToken? {
        if (accessToken == null) {
            accessToken = sharedPrefApi.getObject(SharedPrefApi.ACCESS_TOKEN, AccessToken::class.java)
        }
        return accessToken
    }

    // save accessToken to SharePreference
    fun saveAccessToken(accessToken: AccessToken) {
        this.accessToken = accessToken
        sharedPrefApi.putObject(SharedPrefApi.ACCESS_TOKEN, accessToken)
    }
}

AccessToken class

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)

My Interceptor

class AuthInterceptor @Inject constructor(private val accessTokenWrapper: AccessTokenWrapper): Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val authorisedRequestBuilder = originalRequest.newBuilder()
                .addHeader("Authorization", accessTokenWrapper.getAccessToken()!!.token)
                .header("Accept", "application/json")
        return chain.proceed(authorisedRequestBuilder.build())
    }
}

Finally, add Interceptor and Authenticator to your OKHttpClient when create service PotoAuthApi

Demo

https://github.com/PhanVanLinh/AndroidMVPKotlin

Note

Authenticator flow
  • Example API getImage() return 401 error code
  • authenticate method inside TokenAuthenticator will fired
  • Synchronize noneAuthAPI.refreshToken(...) called
  • After noneAuthAPI.refreshToken(...) response -> new token will add to header
  • getImage() will AUTO called with new header (HttpLogging WILL NOT log this call) (intercept inside AuthInterceptor WILL NOT CALLED)
  • If getImage() still failed with error 401, authenticate method inside TokenAuthenticator will fired AGAIN and AGAIN then it will throw error about call method many time(java.net.ProtocolException: Too many follow-up requests). You can prevent it by count response. Example, if you return null in authenticate after 3 times retry, getImage() will finish and return response 401

  • If getImage() response success => we will result the result normally (like you call getImage() with no error)

Hope it help

like image 22
Linh Avatar answered Oct 14 '22 05:10

Linh