From 169b21cfdb704d5baf76fa9d63c48473e62c6749 Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 17 Nov 2025 14:24:48 -0600 Subject: [PATCH] PM-28053: Ensure any exception thrown during re-auth is an IO exception (#6175) --- .../network/interceptor/AuthTokenManager.kt | 15 +++++++++- .../interceptor/AuthTokenManagerTest.kt | 29 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenManager.kt b/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenManager.kt index 990b4ea229..51aa56bfb4 100644 --- a/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenManager.kt +++ b/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenManager.kt @@ -81,6 +81,7 @@ internal class AuthTokenManager( } } + @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val token = getAccessToken() ?: throw IOException(IllegalStateException(MISSING_TOKEN_MESSAGE)) @@ -96,9 +97,12 @@ internal class AuthTokenManager( } @Synchronized + @Throws(IOException::class) private fun getAccessToken(): String? = authTokenProvider .getAuthTokenDataOrNull() - ?.let { getAccessToken(authTokenData = it).getOrThrow() } + ?.let { authTokenData -> + getAccessToken(authTokenData = authTokenData).getOrElse { throw it.toIoException() } + } @Synchronized private fun getAccessToken(authTokenData: AuthTokenData): Result { @@ -117,3 +121,12 @@ internal class AuthTokenManager( private fun Response.shouldSkipAuthentication(): Boolean = this.priorResponse != null } + +/** + * Helper method to ensure the exception is an [IOException]. + */ +private fun Throwable.toIoException(): IOException = + when (this) { + is IOException -> this + else -> IOException(this) + } diff --git a/network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenManagerTest.kt b/network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenManagerTest.kt index c2acf07a67..08906a4efd 100644 --- a/network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenManagerTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenManagerTest.kt @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import retrofit2.HttpException import java.io.IOException import java.time.Clock import java.time.Instant @@ -197,7 +198,7 @@ class AuthTokenManagerTest { @Suppress("MaxLineLength") @Test - fun `intercept should throw an exception when auth token is expired and refreshAccessTokenSynchronously returns an error`() { + fun `intercept should throw an io exception when auth token is expired and refreshAccessTokenSynchronously returns an error`() { val errorMessage = "Fail!" authTokenManager.refreshTokenProvider = object : RefreshTokenProvider { override fun refreshAccessTokenSynchronously( @@ -216,7 +217,31 @@ class AuthTokenManagerTest { chain = FakeInterceptorChain(request = request), ) } - assertEquals(errorMessage, throwable?.message) + assertEquals(errorMessage, throwable.message) + } + + @Suppress("MaxLineLength") + @Test + fun `intercept should throw a http exception when auth token is expired and refreshAccessTokenSynchronously returns an error`() { + val error = mockk() + authTokenManager.refreshTokenProvider = object : RefreshTokenProvider { + override fun refreshAccessTokenSynchronously( + userId: String, + ): Result = error.asFailure() + } + val authTokenData = AuthTokenData( + userId = USER_ID, + accessToken = ACCESS_TOKEN, + expiresAtSec = FIXED_CLOCK.instant().epochSecond - 3600L, + ) + every { mockAuthTokenProvider.getAuthTokenDataOrNull() } returns authTokenData + + val throwable = assertThrows(IOException::class.java) { + authTokenManager.intercept( + chain = FakeInterceptorChain(request = request), + ) + } + assertEquals(throwable.cause, error) } @Suppress("MaxLineLength")