[Android] Safely Refreshing Tokens with Authenticator and Mutex

2025. 9. 10. 18:05·Android

When building an app with JWT-based authentication, handling Access Token refresh is essential.

In particular, when multiple APIs are called at the same time, unexpected concurrency issues may arise during the refresh process. In this post, I’d like to share the troubleshooting experience I faced while working on my project.

JWT Token System

JWT authentication typically involves two types of tokens:

  • Access Token: Used for actual API requests and has a short lifespan (e.g., 15 minutes) for security.
  • Refresh Token: Used to refresh the Access Token and has a longer lifespan (e.g., 2 weeks).

When the Access Token expires, the server responds with 401 Unauthorized, and the client must use the Refresh Token to obtain a new Access Token.


Implementing with OkHttp Authenticator

OkHttp’s Authenticator works similarly to an Interceptor: it intercepts API responses and invokes the authenticate method whenever a 401 response is received.

  • If a new Request is returned, OkHttp retries the request.
  • If null is returned, the request is not retried.
class TokenAuthenticator(
    private val tokenManager: TokenManager,
    private val authApiService: AuthApiService,
) : Authenticator {
    override fun authenticate(
        route: Route?,
        response: Response,
    ): Request? {
        return runBlocking {
            val refreshToken: String = tokenManager.getRefreshToken() ?: return@runBlocking null
            
            safeApiCall {
                val tokenRequest = TokenRequest(refreshToken)
                authApiService.postRefresh(tokenRequest)
            }.fold(
                onSuccess = { (accessToken, refreshToken) ->
                    tokenManager.saveTokens(accessToken, refreshToken)
                    response.request.addTokenHeader(accessToken)
                },
                onFailure = {
                    tokenManager.clearTokens()
                    null
                },
            )
        }
    }
}

 

When Access token expires:

  1. A 401 response triggers the authenticate method.
  2. A new token is issued using the Refresh Token.
  3. The failed request is retried with the new Access Token.

At first, this worked as expected. Even after 15 minutes, expired tokens were seamlessly refreshed.

Or so it seemed…

 

The Unexpected Concurrency Issue

In the “Yagubogu” app’s home screen, more than five APIs are called asynchronously to load various data.

private fun fetchMemberStats(year: Int = LocalDate.now().year) {
    viewModelScope.launch {
        val myTeamDeferred: Deferred<Result<String?>> =
            async { memberRepository.getFavoriteTeam() }
        val attendanceCountDeferred: Deferred<Result<Int>> =
            async { checkInRepository.getCheckInCounts(year) }
        val winRateDeferred: Deferred<Result<Double>> =
            async { statsRepository.getStatsWinRate(year) }
        // ...
    }
}

 

Here’s the critical problem:

If the Access Token has been expired when entering the home screen, each API call runs in its own coroutine (thread). As a result, Authenticator was invoked simultaneously for every request.

  • Five API calls receive 401 responses.
  • Each triggers its own authenticate method independently.
  • This causes five concurrent refresh requests, all of which failed.

Synchronizing with Mutex

A Mutex (mutual exclusion) is a synchronization technique that prevents multiple threads or coroutines from accessing a shared resource simultaneously.

In coroutines, a Mutex suspends only the coroutine—not the thread. The first coroutine to acquire the lock executes first, and a coroutine that already holds the lock cannot request it again.

 

class TokenAuthenticator(
    private val tokenManager: TokenManager,
    private val tokenApiService: TokenApiService,
) : Authenticator {
    private val mutex = Mutex()

    override fun authenticate(
        route: Route?,
        response: Response,
    ): Request? {
        val request: Request = response.request
        return runBlocking {
            mutex.withLock {
                val invalidToken: String? = request.getTokenFromHeader()
                val currentToken: String? = tokenManager.getAccessToken()

                if (currentToken != null && currentToken != invalidToken) {
                    return@withLock request.addTokenHeader(currentToken)
                }

                val (newAccessToken: String, newRefreshToken: String) =
                    refreshAccessToken() ?: return@withLock null
                tokenManager.saveTokens(newAccessToken, newRefreshToken)
                request.addTokenHeader(newAccessToken)
            }
        }
    }

    private suspend fun refreshAccessToken(): TokenResponse? {
        val refreshToken: String = tokenManager.getRefreshToken() ?: return null
        return safeApiCall {
            tokenApiService.postRefresh(TokenRequest(refreshToken))
        }.getOrElse {
            tokenManager.clearTokens()
            null
        }
    }
}

 

By using a Mutex, only one coroutine at a time can execute the token refresh logic.

Additionally, if another request has already refreshed the token, subsequent requests can immediately reuse the new token, avoiding duplicate refresh calls.


This approach ensured safe, synchronized token refresh handling even under heavy concurrent API calls.

 

'Android' 카테고리의 다른 글

Retrofit에서 Ktor로: 안드로이드 HTTP 클라이언트 마이그레이션  (0) 2026.01.08
[Android] Improving BottomNavigation Fragment Switching Performance  (0) 2025.08.13
[Android] When Exactly Is onPause() Called?  (3) 2025.07.22
'Android' 카테고리의 다른 글
  • Retrofit에서 Ktor로: 안드로이드 HTTP 클라이언트 마이그레이션
  • [Android] Improving BottomNavigation Fragment Switching Performance
  • [Android] When Exactly Is onPause() Called?
jiyuneel
jiyuneel
  • jiyuneel
    Yuun
    jiyuneel
  • 전체
    오늘
    어제
    • 분류 전체보기 (6)
      • Android (4)
      • Kotlin (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Android Studio
    Android
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
jiyuneel
[Android] Safely Refreshing Tokens with Authenticator and Mutex
상단으로

티스토리툴바