In `okhttp` how do I trigger `Authenticator` if ac...
# squarelibraries
u
In
okhttp
how do I trigger
Authenticator
if access tokens are deemed expired locally? (to save on traffic that will most likely end up in 401 - and to refresh & retry right away)
Copy code
class AccessTokenInterceptor(private val provider: Provider) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val accessToken = provider.get()
        return if (accessToken == null) {
            chain.proceed(request)
        } else {
            if (accessToken.isExpired(clock)) {
                Response.Builder() <----------------------------------
                    .protocol(Protocol.HTTP_1_1)
                    .message("foo")
                    .request(request)
                    .code(401)
                    .build()
            } else {
                chain.proceed(
                    request
                        .newBuilder
                        .header("Authorization", "Bearer ${accessToken.value}")
                        .build()
                )
            }
        }
    }
I'm trying to return a dummy 401 response but
Authenticator
is not triggered
y
Is it installed as a network interceptor?
u
the interceptor? no
Copy code
appOkHttp.newBuilder()
    .addInterceptor(AccessTokenInterceptor(...))
    .authenticator(AccessTokenAuthenticator(...))
    .build()
should it?
I changed the interceptor registration to
networkInterceptor
and now I'm getting
Copy code
java.io.IOException: canceled due to java.lang.IllegalStateException: network interceptor sk.o2.auth.interceptor.AccessTokenInterceptor@97f9958 must call proceed() exactly once
                 	at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:587)
                 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
                 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
                 	at java.lang.Thread.run(Thread.java:1012)
which means..I cannot not call proceed, so I have to make the actual request?
y
ahhh, yep. Sorry, bad advice.
The reason I suggested it is the in built authentication is done after application interceptors here https://github.com/square/okhttp/blob/e4ae9b02e913deec3a9da4c142064059436374fb/okh[…]oid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt
u
btw genuinely, is this such a nonstandard request? I never done this in a app before - but these backend folks were looking at me like I'm crazy that I'm not checking the validity locally ahead of time
y
You can easily do it in a normal app interceptor, just not behind the authenticator API.
u
but I like my authenticator 😄 do you think it's feasible or do I tell them it's not without refactors?
y
it's not a big refactor, just adding a preemptive authenticator via an interceptor.
There are a bunch of questions like this, but because the workaround is easy with an interceptor, preemptive authentication is not explicitly supported. search for preemptive in the issues and you'll see the questions.
u
do you mean a authenticator, or the
okhttp3.Authenticator
?
y
I meant whatever your authenticator is doing call it from an application interceptor for hosts that need auth
The OkHttp Authenticator needs a response as a param, not a request, so it can't work preemptively
u
> needs a response as a param this is where I stuff the synthetic 401?
Copy code
class AccessTokenInterceptor(
    private val provider: Provider,
    private val authenticator: Authenticator,
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val accessToken = provider.get()

        return if (accessToken.isExpired) {
            val synthetic401Response = Response.Builder()
                .protocol(Protocol.HTTP_1_1)
                .message("Synthetic unauthorized")
                .request(request)
                .code(401)
                .build()

            val reauthRequest = authenticator.authenticate(
                route = chain.connection()?.route(),
                response = synthetic401Response <-------------
            )

            if (reauthRequest == null) {
                synthetic401Response <-------------- specifically this bit
            } else {
                chain.proceed(reauthRequest)
            }
        } else {
            chain.proceed(request.authorized(accessToken, isReauth = false))
        }
    }

    interface Provider {
        fun get(): AccessToken?
    }
}
does this look right to you please? responses need to be closed right?
j
You don't need a synthetic 401
Just create an interceptor that adds your authentication headers proactively
The interceptor API can do more than the Authenticator API
u
I am adding the auth headers proactively. The specific q here is how to proactivelly trigger refresh if locally the token seems expired (OIDC token expiry in the payload) Or that a bad idea to want to save such traffic?
j
Yeah you can have a Interceptor that keeps some policy like "refresh the tokens if they are half-done their lifetime (time of expiry minus time of issue, divided by two)
And when they reach that point, refresh the tokens
u
j
Half works fine if your server gives you tokens that last 1 hour or 1 year
u
I don't follow, why not trigger refresh only when actually expired (fully) as per the
token.expiry
? (My actual case is that backends changed identity provider and now access tokens are only 5minutes long, so they're getting a lot of requests from the app that end up 401'ed - and would me to not cause such traffic, refresh right away if I'm locally confident it's expired) But it's just a optimization, not a hard requirement, reacting to actual 401 still should work as standard
j
It might be a better user experience for API calls to never 401, because every 401 delays the actual result
And you can avoid 401s by proactively renewing tokens
you'll spend more bandwidth doing token renew calls, but practically not much more because the calls that 401 also spend bandwidth
u
fair could you then please take a look at the snippet? Im not sure about the response closing there + if authenticator returns null if just returning the syntethic to the caller is fine
j
(waiting for a 401 ends up as a winning strategy if your calls are less frequent then your renew period)
u
as not to leak stuff
j
I cannot look at the snippet but only cause Im on a phone
u
maybe an image might help? 😄 but okay no pressure, thank you a lot for the comments