OAuth
Retrofit
Token Refresh
API Authentication
Android Development

Refreshing OAuth token using Retrofit without modifying all calls

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

The clean way to refresh OAuth tokens in Retrofit is to centralize the logic in OkHttp rather than touching every API call. In practice, that means one interceptor for attaching the current access token and one authenticator for reacting to 401 responses by refreshing and retrying the failed request.

Separate request decoration from refresh logic

These two jobs should not be mixed:

  • add the current token to outgoing requests
  • refresh the token when the server rejects it

The first belongs in an interceptor. The second belongs in an authenticator.

Add the token with an interceptor

The interceptor reads the current access token from a repository and attaches it to every request.

kotlin
1class AuthHeaderInterceptor(
2    private val tokenStore: TokenStore
3) : Interceptor {
4    override fun intercept(chain: Interceptor.Chain): Response {
5        val token = tokenStore.accessToken()
6        val request = chain.request().newBuilder()
7            .apply {
8                if (token != null) {
9                    header("Authorization", "Bearer $token")
10                }
11            }
12            .build()
13
14        return chain.proceed(request)
15    }
16}

This keeps your Retrofit service interfaces free of repetitive token boilerplate.

Refresh with an Authenticator

When the server returns 401, OkHttp can invoke an Authenticator. That is the right place to refresh and retry.

kotlin
1class TokenAuthenticator(
2    private val tokenStore: TokenStore,
3    private val authApi: AuthApi
4) : Authenticator {
5
6    @Synchronized
7    override fun authenticate(route: Route?, response: Response): Request? {
8        if (responseCount(response) >= 2) {
9            return null
10        }
11
12        val currentToken = tokenStore.accessToken()
13        val requestToken = response.request.header("Authorization")
14            ?.removePrefix("Bearer ")
15
16        if (currentToken != null && currentToken != requestToken) {
17            return response.request.newBuilder()
18                .header("Authorization", "Bearer $currentToken")
19                .build()
20        }
21
22        val refreshToken = tokenStore.refreshToken() ?: return null
23        val refreshResult = authApi.refreshToken(RefreshRequest(refreshToken)).execute()
24
25        if (!refreshResult.isSuccessful) {
26            tokenStore.clear()
27            return null
28        }
29
30        val body = refreshResult.body() ?: return null
31        tokenStore.save(body.accessToken, body.refreshToken)
32
33        return response.request.newBuilder()
34            .header("Authorization", "Bearer ${body.accessToken}")
35            .build()
36    }
37
38    private fun responseCount(response: Response): Int {
39        var count = 1
40        var prior = response.priorResponse
41        while (prior != null) {
42            count++
43            prior = prior.priorResponse
44        }
45        return count
46    }
47}

The synchronized block prevents several concurrent 401 responses from all refreshing at once.

Wire it into Retrofit once

The client setup is where the whole design comes together:

kotlin
1val okHttpClient = OkHttpClient.Builder()
2    .addInterceptor(AuthHeaderInterceptor(tokenStore))
3    .authenticator(TokenAuthenticator(tokenStore, authApi))
4    .build()
5
6val retrofit = Retrofit.Builder()
7    .baseUrl("https://api.example.com/")
8    .client(okHttpClient)
9    .addConverterFactory(MoshiConverterFactory.create())
10    .build()

Now the token handling is centralized and existing API interfaces usually need no changes.

Keep the refresh endpoint separate

A practical detail is that the refresh call itself should not recurse through the same failed auth flow. Many teams solve this by:

  • using a dedicated Retrofit instance for auth refresh
  • excluding the refresh request from the normal auth header logic

If you use the exact same stack carelessly, a failed refresh can loop into more refresh attempts.

Handle logout and terminal failure explicitly

If refresh fails because the refresh token is expired or revoked, return null from the authenticator so OkHttp stops retrying. Then clear local auth state and trigger a re-login flow.

A refresh system is incomplete if it can refresh successfully but has no clean failure path.

Common Pitfalls

The most common mistake is trying to refresh tokens manually inside every Retrofit call or repository method, which spreads authentication logic across the whole codebase. Another is putting refresh logic in an interceptor rather than an authenticator, which makes retry flow harder to reason about. Developers also often forget to serialize refresh attempts, so multiple 401 responses trigger several simultaneous refresh calls. Using the same Retrofit client for the refresh endpoint without any guard can cause recursive failure loops. Finally, many implementations refresh successfully but do not define what should happen when refresh fails permanently.

Summary

  • Use an interceptor to attach the current access token to requests.
  • Use an OkHttp authenticator to refresh after 401 responses.
  • Keep token storage centralized in one repository or store.
  • Prevent concurrent refresh storms with synchronization or another single-flight mechanism.
  • Use a separate auth path for the refresh endpoint when needed.
  • Define a clear logout or re-authentication path for refresh failure.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.