PM-24481: Logout when token refresh API returns 401 or 403 (#5651)

This commit is contained in:
David Perez
2025-08-06 15:38:01 -05:00
committed by GitHub
parent 59c2261e7c
commit 3c033d4aa2
8 changed files with 158 additions and 16 deletions

View File

@@ -40,4 +40,18 @@ sealed class RefreshTokenResponseJson {
) : RefreshTokenResponseJson() {
val isInvalidGrant: Boolean get() = error == "invalid_grant"
}
/**
* Models a failure response with a 403 "Forbidden" response code.
*/
data class Forbidden(
val error: Throwable,
) : RefreshTokenResponseJson()
/**
* Models a failure response with a 401 "Unauthorized" response code.
*/
data class Unauthorized(
val error: Throwable,
) : RefreshTokenResponseJson()
}

View File

@@ -20,6 +20,7 @@ import com.bitwarden.network.util.DeviceModelProvider
import com.bitwarden.network.util.NetworkErrorCode
import com.bitwarden.network.util.base64UrlEncode
import com.bitwarden.network.util.executeForNetworkResult
import com.bitwarden.network.util.getNetworkErrorCodeOrNull
import com.bitwarden.network.util.parseErrorBodyOrNull
import com.bitwarden.network.util.toResult
import kotlinx.serialization.json.Json
@@ -131,13 +132,28 @@ internal class IdentityServiceImpl(
.executeForNetworkResult()
.toResult()
.recoverCatching { throwable ->
throwable
.toBitwardenError()
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<RefreshTokenResponseJson.Error>(
code = NetworkErrorCode.BAD_REQUEST,
json = json,
)
?: throw throwable
?: run {
when (bitwardenError.getNetworkErrorCodeOrNull()) {
NetworkErrorCode.UNAUTHORIZED -> {
RefreshTokenResponseJson.Unauthorized(throwable)
}
NetworkErrorCode.FORBIDDEN -> {
RefreshTokenResponseJson.Forbidden(throwable)
}
NetworkErrorCode.BAD_REQUEST,
NetworkErrorCode.TOO_MANY_REQUESTS,
null,
-> throw throwable
}
}
}
override suspend fun registerFinish(

View File

@@ -5,6 +5,14 @@ import com.bitwarden.network.model.BitwardenError
import kotlinx.serialization.json.Json
import retrofit2.HttpException
/**
* Returns the [NetworkErrorCode] for the given error if it is available.
*/
internal fun BitwardenError.getNetworkErrorCodeOrNull(): NetworkErrorCode? =
(this as? BitwardenError.Http)?.let { httpError ->
NetworkErrorCode.entries.firstOrNull { httpError.code == it.code }
}
/**
* Attempt to parse the error body to serializable type [T].
*

View File

@@ -7,5 +7,7 @@ internal enum class NetworkErrorCode(
val code: Int,
) {
BAD_REQUEST(code = 400),
UNAUTHORIZED(code = 401),
FORBIDDEN(code = 403),
TOO_MANY_REQUESTS(code = 429),
}

View File

@@ -333,6 +333,20 @@ class IdentityServiceTest : BaseServiceTest() {
assertTrue(result.isFailure)
}
@Test
fun `refreshTokenSynchronously when response is a 403 error should return an Forbidden`() {
server.enqueue(MockResponse().setResponseCode(403))
val result = identityService.refreshTokenSynchronously(refreshToken = REFRESH_TOKEN)
assertTrue(result.getOrThrow() is RefreshTokenResponseJson.Forbidden)
}
@Test
fun `refreshTokenSynchronously when response is a 401 error should return an Unauthorized`() {
server.enqueue(MockResponse().setResponseCode(401))
val result = identityService.refreshTokenSynchronously(refreshToken = REFRESH_TOKEN)
assertTrue(result.getOrThrow() is RefreshTokenResponseJson.Unauthorized)
}
@Test
fun `registerFinish success json should be Success`() = runTest {
val expectedResponse = RegisterResponseJson.Success(