Actualización del token OAuth usando Retrofit sin modificar todas las llamadas

157

Estamos utilizando Retrofit en nuestra aplicación de Android para comunicarnos con un servidor seguro OAuth2. Todo funciona muy bien, utilizamos RequestInterceptor para incluir el token de acceso con cada llamada. Sin embargo, habrá momentos en los que el token de acceso caducará y el token debe actualizarse. Cuando el token caduca, la próxima llamada regresará con un código HTTP no autorizado, por lo que es fácil de monitorear. Podríamos modificar cada llamada de Retrofit de la siguiente manera: en la devolución de llamada fallida, verifique el código de error, si es igual a No autorizado, actualice el token OAuth, luego repita la llamada de Retrofit. Sin embargo, para esto, todas las llamadas deben modificarse, lo cual no es una solución fácil de mantener y buena. ¿Hay alguna manera de hacer esto sin modificar todas las llamadas de Retrofit?

Daniel Zolnai
fuente
1
Esto parece relevante para mi otra pregunta . Lo examinaré nuevamente pronto, pero un enfoque posible es envolver OkHttpClient. Algo como esto: github.com/pakerfeldt/signpost-retrofit Además, dado que estoy usando RoboSpice con Retrofit, crear una clase de Solicitud base también puede ser otro enfoque posible. Sin embargo, probablemente tenga que descubrir cómo lograr su flujo sin un Contexto, tal vez utilizando Otto / EventBus.
Hassan Ibraheem
1
Bueno, podría bifurcarlo y eliminar los casos innecesarios. Tal vez lo investigue hoy y publique aquí si logro algo que pueda resolver nuestro problema.
Daniel Zolnai
2
Resultó que la biblioteca no manejaba tokens refrescantes, pero me dio una idea. Hice una pequeña idea sobre un código no probado, pero en teoría, creo que debería funcionar: gist.github.com/ZolnaiDani/9710849
Daniel Zolnai
3
@neworld Una solución que se me ocurre: sincronice changeTokenInRequest (...) y, en la primera línea, compruebe cuándo fue la última vez que se actualizó el token. Si solo han pasado unos segundos (milisegundos), no actualice el token. También puede establecer este período de tiempo en aproximadamente 1 hora, para dejar de solicitar constantemente nuevos tokens cuando hay otro problema fuera del token que está desactualizado.
Daniel Zolnai
2
Retrofit 1.9.0 acaba de agregar soporte para OkHttp 2.2, que tiene interceptores. Esto debería hacer su trabajo mucho más fácil. Para obtener más información, consulte: github.com/square/retrofit/blob/master/… y github.com/square/okhttp/wiki/Interceptors Sin embargo, también debe extender OkHttp para estos.
Daniel Zolnai

Respuestas:

214

No lo use Interceptorspara tratar con la autenticación.

Actualmente, el mejor enfoque para manejar la autenticación es utilizar la nueva AuthenticatorAPI, diseñada específicamente para este propósito .

OkHttp solicitará automáticamente las Authenticatorcredenciales cuando una respuesta 401 Not Authorised vuelva a intentar la última solicitud fallida con ellas.

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

Adjunte una Authenticatora de OkHttpClientla misma manera que lo hace conInterceptors

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

Use este cliente al crear su Retrofit RestAdapter

RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(ENDPOINT)
                .setClient(new OkClient(okHttpClient))
                .build();
return restAdapter.create(API.class);
lgvalle
fuente
66
¿Significa esto que cada solicitud fallará siempre 1 vez o agrega el token al hacer solicitudes?
Jdruwe
11
@Jdruwe parece que este código fallará 1 vez, y luego hará la solicitud. sin embargo, si agrega un interceptor cuyo único propósito es agregar siempre un token de acceso (independientemente de si ha expirado o no), esto solo se llamará cuando se reciba un 401, que solo ocurrirá cuando ese token haya expirado.
narciero
54
TokenAuthenticatordepende de una una serviceclase. La serviceclase depende de una OkHttpClientinstancia. Para crear un OkHttpClientnecesito el TokenAuthenticator. ¿Cómo puedo romper este ciclo? ¿Dos OkHttpClients diferentes ? Tendrán diferentes grupos de conexión ...
Brais Gabin
66
¿Qué tal muchas solicitudes paralelas que necesitan actualizar el token? Habrá muchas solicitudes de token de actualización al mismo tiempo. ¿Cómo evitarlo?
Igor Kostenko
10
Ok, entonces la solución para el problema de @ Ihor podría ser sincronizar el código dentro de Authenticator. Resolvió el problema en mi caso. en el método Solicitar autenticación (...): - haga cualquier cosa de inicialización - inicie el bloque sincronizado (sincronizado (MyAuthenticator.class) {...}) - en ese bloque recupere el token de acceso actual y de actualización - compruebe si la solicitud fallida estaba utilizando la última versión token de acceso (resp.request (). header ("Autorización")) - si no, simplemente ejecútelo una vez más con el token de acceso actualizado - de lo contrario, ejecute el flujo de token de actualización - actualice / persista el acceso actualizado y el token de actualización - finalice el bloque sincronizado - vuelva a ejecutar
Dariusz Wiechecki
65

Si está utilizando Retrofit > = 1.9.0, podría utilizar el nuevo Interceptor de OkHttp , que se introdujo en OkHttp 2.2.0. Desea utilizar un interceptor de aplicaciones , que le permite retry and make multiple calls.

Su interceptor podría parecerse a este pseudocódigo:

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

            // 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;
    }

}

Después de definir su Interceptor, cree OkHttpClienty agregue el interceptor como un interceptor de aplicación .

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

Y finalmente, use esto OkHttpClientcuando cree su RestAdapter.

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

Advertencia: como Jesse Wilson(de Square) menciona aquí , esta es una cantidad peligrosa de energía.

Dicho esto, definitivamente creo que esta es la mejor manera de manejar algo como esto ahora. Si tiene alguna pregunta, no dude en hacer un comentario.

theblang
fuente
2
¿Cómo está logrando una llamada síncrona en Android cuando Android no permite llamadas de red en el hilo principal? Tengo problemas para devolver una respuesta de una llamada asincrónica.
lgdroid57
1
@ lgdroid57 Tiene razón, por lo que ya debería estar en otro hilo cuando inició la solicitud original que activó la ejecución del interceptor.
theblang
3
Esto funcionó muy bien, excepto que tuve que asegurarme de cerrar la respuesta anterior o filtraría la conexión anterior ... Solicitud final newRequest = request.newBuilder () .... build (); response.body (). close (); return chain.proceed (newRequest);
DallinDyer
¡Gracias! Me encontraba con un problema en el que la devolución de llamada de la solicitud original recibía un mensaje de error de "cerrado" en lugar de la respuesta original, debido a que el cuerpo se consumía en el interceptor. Pude arreglar esto para respuestas exitosas, pero no pude arreglar esto para respuestas fallidas. ¿Alguna sugerencia?
lgdroid57
Gracias @mattblang, se ve bien. Una pregunta: ¿se garantiza que la devolución de llamada de solicitud se invoque incluso en el reintento?
Luca Fagioli
23

TokenAuthenticator depende de una clase de servicio. La clase de servicio depende de una instancia OkHttpClient. Para crear un OkHttpClient, necesito el TokenAuthenticator. ¿Cómo puedo romper este ciclo? ¿Dos OkHttpClients diferentes? Tendrán diferentes grupos de conexión.

Si tiene, por ejemplo, un Retrofit TokenServiceque necesita dentro de su, Authenticatorpero solo le gustaría configurar uno para el OkHttpClientque pueda usar TokenServiceHoldercomo dependencia TokenAuthenticator. Tendría que mantener una referencia a él en el nivel de aplicación (singleton). Esto es fácil si está utilizando Dagger 2, de lo contrario, simplemente cree un campo de clase dentro de su aplicación.

En 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;
    }

En TokenServiceHolder.java:

public class TokenServiceHolder {

    TokenService tokenService = null;

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

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

Configuración del cliente:

//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);

Si está utilizando Dagger 2 o un marco de inyección de dependencia similar, hay algunos ejemplos en las respuestas a esta pregunta

David Rawson
fuente
¿Dónde se TokenServicecrea la clase?
Yogesh Suthar
@YogeshSuthar es un servicio de modificación - vea la pregunta relacionada
David Rawson
Gracias, ¿puede proporcionar la implementación de refreshToken()from service.refreshToken().execute();? No puedo encontrar su implementación en ningún lado.
Yogesh Suthar
@Yogesh El método refreshToken es de su API. Lo que sea que llame para actualizar un token (¿una llamada con nombre de usuario y contraseña tal vez?). O tal vez una solicitud donde envía un token y la respuesta es un token nuevo
David Rawson
5

Usar TokenAuthenticatorlike @theblang answer es una forma correcta de manejar refresh_token.

Aquí está mi implemento (he usado Kotlin, Dagger, RX pero puede usar esta idea para implementar en su caso)
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()
    }
}

Para evitar el ciclo de dependencia como el comentario de @Brais Gabin, creo 2 interfaces como

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

y

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

AccessTokenWrapper clase

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 clase

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)

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

Finalmente, agregue Interceptory Authenticatora su OKHttpClientcuando cree el servicio PotoAuthApi

Manifestación

https://github.com/PhanVanLinh/AndroidMVPKotlin

Nota

Autenticador de flujo
  • Ejemplo de API getImage()devuelve el código de error 401
  • authenticatemétodo en el interior TokenAuthenticatorserá despedido
  • Sincronizar noneAuthAPI.refreshToken(...)llamado
  • Después de la noneAuthAPI.refreshToken(...)respuesta -> nuevo token se agregará al encabezado
  • getImage()llamará AUTO con un nuevo encabezado ( HttpLogging NO registrará esta llamada) ( interceptdentro AuthInterceptor NO LLAMARÁ )
  • Si getImage()todavía se produjo el error 401, authenticatemétodo en el interior TokenAuthenticatorvoluntad disparó una y otra vez , entonces va a tirar de error sobre el método llamado muchas veces ( java.net.ProtocolException: Too many follow-up requests). Puede prevenirlo por conteo de respuesta . Ejemplo, si return nullen authenticatelos 3 tiempos de reintento, getImage()será terminar yreturn response 401

  • Si la getImage()respuesta es exitosa => obtendremos el resultado normalmente (como usted llama getImage()sin error)

Espero que ayude

Phan Van Linh
fuente
Esta solución usa 2 OkHttpClients diferentes, como es evidente en su clase ServiceGenerator.
SpecialSnowflake
@SpecialSnowflake tienes razón. Si sigues mi solución, necesitas crear 2 OkHttp porque creé 2 servicios (oauth y none auth). Creo que no causará ningún problema. Déjame saber tu idea
Phan Van Linh
1

Sé que este es un hilo viejo, pero por si alguien tropezó con él.

TokenAuthenticator depende de una clase de servicio. La clase de servicio depende de una instancia OkHttpClient. Para crear un OkHttpClient, necesito el TokenAuthenticator. ¿Cómo puedo romper este ciclo? ¿Dos OkHttpClients diferentes? Tendrán diferentes grupos de conexión.

Estaba enfrentando el mismo problema, pero quería crear solo un OkHttpClient porque no creo que necesite otro solo para el TokenAuthenticator en sí, estaba usando Dagger2, así que terminé brindando la clase de servicio cuando Lazy inyectó en el TokenAuthenticator, puedes leer más sobre la inyección diferida en Dagger 2 aquí , pero es básicamente como decirle a Dagger que NO vaya y cree el servicio que TokenAuthenticator necesita de inmediato.

Puede consultar este hilo SO para obtener un código de muestra: ¿Cómo resolver una dependencia circular mientras todavía usa Dagger2?

Boda
fuente
0

Puede intentar crear una clase base para todos sus cargadores en la que pueda detectar una excepción particular y luego actuar según lo necesite. Haga que todos sus cargadores diferentes se extiendan desde la clase base para difundir el comportamiento.

k3v1n4ud3
fuente
La actualización no funciona de esa manera. Utiliza anotaciones de Java e interfaces para describir una llamada API
Daniel Zolnai
Sé cómo funciona la actualización, pero todavía estás "envolviendo" tus llamadas API dentro de un AsynTask, ¿no?
k3v1n4ud3
No, uso las llamadas con una devolución de llamada, por lo que se ejecutan de forma asincrónica.
Daniel Zolnai
Entonces, probablemente pueda crear una clase de devolución de llamada base y hacer que todas sus devoluciones de llamada la extiendan.
k3v1n4ud3
2
¿Alguna solución a esto? Es exactamente mi caso aquí. = /
Hugo Nogueira
0

Después de una larga investigación, personalicé el cliente Apache para manejar Refreshing AccessToken For Retrofit en el que envía token de acceso como parámetro.

Inicie su adaptador con cookie Persistent Client

restAdapter = new RestAdapter.Builder()
                .setEndpoint(SERVER_END_POINT)
                .setClient(new CookiePersistingClient())
                .setLogLevel(RestAdapter.LogLevel.FULL).build();

Cookie Cliente persistente que mantiene cookies para todas las solicitudes y verifica con cada respuesta de solicitud, si es acceso no autorizado ERROR_CODE = 401, actualiza el token de acceso y recupera la solicitud, de lo contrario solo procesa la solicitud.

private static class CookiePersistingClient extends ApacheClient {

    private static final int HTTPS_PORT = 443;
    private static final int SOCKET_TIMEOUT = 300000;
    private static final int CONNECTION_TIMEOUT = 300000;

    public CookiePersistingClient() {
        super(createDefaultClient());
    }

    private static HttpClient createDefaultClient() {
        // Registering https clients.
        SSLSocketFactory sf = null;
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore
                    .getDefaultType());
            trustStore.load(null, null);

            sf = new MySSLSocketFactory(trustStore);
            sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params,
                CONNECTION_TIMEOUT);
        HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("https", sf, HTTPS_PORT));
        // More customization (https / timeouts etc) can go here...

        ClientConnectionManager cm = new ThreadSafeClientConnManager(
                params, registry);
        DefaultHttpClient client = new DefaultHttpClient(cm, params);

        // Set the default cookie store
        client.setCookieStore(COOKIE_STORE);

        return client;
    }

    @Override
    protected HttpResponse execute(final HttpClient client,
            final HttpUriRequest request) throws IOException {
        // Set the http context's cookie storage
        BasicHttpContext mHttpContext = new BasicHttpContext();
        mHttpContext.setAttribute(ClientContext.COOKIE_STORE, COOKIE_STORE);
        return client.execute(request, mHttpContext);
    }

    @Override
    public Response execute(final Request request) throws IOException {
        Response response = super.execute(request);
        if (response.getStatus() == 401) {

            // Retrofit Callback to handle AccessToken
            Callback<AccessTockenResponse> accessTokenCallback = new Callback<AccessTockenResponse>() {

                @SuppressWarnings("deprecation")
                @Override
                public void success(
                        AccessTockenResponse loginEntityResponse,
                        Response response) {
                    try {
                        String accessToken =  loginEntityResponse
                                .getAccessToken();
                        TypedOutput body = request.getBody();
                        ByteArrayOutputStream byte1 = new ByteArrayOutputStream();
                        body.writeTo(byte1);
                        String s = byte1.toString();
                        FormUrlEncodedTypedOutput output = new FormUrlEncodedTypedOutput();
                        String[] pairs = s.split("&");
                        for (String pair : pairs) {
                            int idx = pair.indexOf("=");
                            if (URLDecoder.decode(pair.substring(0, idx))
                                    .equals("access_token")) {
                                output.addField("access_token",
                                        accessToken);
                            } else {
                                output.addField(URLDecoder.decode(
                                        pair.substring(0, idx), "UTF-8"),
                                        URLDecoder.decode(
                                                pair.substring(idx + 1),
                                                "UTF-8"));
                            }
                        }
                        execute(new Request(request.getMethod(),
                                request.getUrl(), request.getHeaders(),
                                output));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }

                @Override
                public void failure(RetrofitError error) {
                    // Handle Error while refreshing access_token
                }
            };
            // Call Your retrofit method to refresh ACCESS_TOKEN
            refreshAccessToken(GRANT_REFRESH,CLIENT_ID, CLIENT_SECRET_KEY,accessToken, accessTokenCallback);
        }

        return response;
    }
}
Suneel Prakash
fuente
¿Hay alguna razón por la que está usando ApacheClient en lugar de la solución sugerida? No es que no sea una buena solución, pero necesita mucha más codificación, en comparación con el uso de interceptores.
Daniel Zolnai
Está personalizado para ser cliente persistente de cookies, mantiene la sesión en todos los servicios. Incluso en Request Intercceptor, puede agregar token de acceso en los encabezados. Pero, ¿qué pasa si quieres agregarlo como parámetro? También OKHTTPClient tiene limitaciones. ref: stackoverflow.com/questions/24594823/…
Suneel Prakash
Es más generalizado para ser utilizado en cualquier caso 1. Cookie Persistent Client 2. Acepta solicitudes HTTP y HTTPS 3. Actualice el token de acceso en los parámetros.
Suneel Prakash
0

El uso de un Interceptor (inyectar el token) y un Autenticador (operaciones de actualización) hacen el trabajo pero:

También tuve un problema de doble llamada: la primera llamada siempre devolvió un 401 : el token no se inyectó en la primera llamada (interceptor) y se llamó al autenticador: se realizaron dos solicitudes.

La solución fue solo para volver a efectuar la solicitud a la compilación en el Interceptor:

ANTES DE:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

DESPUÉS:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request = request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

EN UN BLOQUE:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request().newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

Espero eso ayude.

Editar: no encontré una manera de evitar la primera llamada para siempre devolver 401 usando solo el autenticador y sin interceptor

Sigrun
fuente
-2

Para cualquiera que quisiera resolver llamadas simultáneas / paralelas al actualizar el token. Aquí hay una solución

class TokenAuthenticator: Authenticator {

    override fun authenticate(route: Route?, response: Response?): Request? {
        response?.let {
            if (response.code() == 401) {
                while (true) {
                    if (!isRefreshing) {
                        val requestToken = response.request().header(AuthorisationInterceptor.AUTHORISATION)
                        val currentToken = OkHttpUtil.headerBuilder(UserService.instance.token)

                        currentToken?.let {
                            if (requestToken != currentToken) {
                                return generateRequest(response, currentToken)
                            }
                        }

                        val token = refreshToken()
                        token?.let {
                            return generateRequest(response, token)
                        }
                    }
                }
            }
        }

        return null
    }

    private fun generateRequest(response: Response, token: String): Request? {
        return response.request().newBuilder()
                .header(AuthorisationInterceptor.USER_AGENT, OkHttpUtil.UA)
                .header(AuthorisationInterceptor.AUTHORISATION, token)
                .build()
    }

    private fun refreshToken(): String? {
        synchronized(TokenAuthenticator::class.java) {
            UserService.instance.token?.let {
                isRefreshing = true

                val call = ApiHelper.refreshToken()
                val token = call.execute().body()
                UserService.instance.setToken(token, false)

                isRefreshing = false

                return OkHttpUtil.headerBuilder(token)
            }
        }

        return null
    }

    companion object {
        var isRefreshing = false
    }
}
Anders Cheow
fuente