diff --git a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt index bc42c32f7..6227b7a1e 100644 --- a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt +++ b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt @@ -22,9 +22,11 @@ internal class JwtProviderImpl @Inject constructor( private val tokenMutex = Mutex() private var _cachedAccessToken: AccessToken? = null + override val cachedAccessToken: AccessToken get() { - val accessToken = _cachedAccessToken ?: throw CannotUseAccessTokenException() + val accessToken = + _cachedAccessToken ?: throw CannotUseAccessTokenException() if (accessToken.isExpired()) { throw CannotUseAccessTokenException() @@ -33,25 +35,29 @@ internal class JwtProviderImpl @Inject constructor( return accessToken } - private val _isCachedAccessTokenAvailable: MutableStateFlow = + private val _isCachedAccessTokenAvailable = MutableStateFlow(checkIsAccessTokenAvailable()) + override val isCachedAccessTokenAvailable: StateFlow = _isCachedAccessTokenAvailable.asStateFlow() private var _cachedRefreshToken: RefreshToken? = null + override val cachedRefreshToken: RefreshToken get() { - if (_cachedRefreshToken == null) { - throw CannotUseRefreshTokenException() - } - if (_cachedRefreshToken!!.isExpired()) { + val refreshToken = + _cachedRefreshToken ?: throw CannotUseRefreshTokenException() + + if (refreshToken.isExpired()) { throw CannotUseRefreshTokenException() } - return _cachedRefreshToken!! + + return refreshToken } - private val _isCachedRefreshTokenAvailable: MutableStateFlow = + private val _isCachedRefreshTokenAvailable = MutableStateFlow(checkIsRefreshTokenAvailable()) + override val isCachedRefreshTokenAvailable: StateFlow = _isCachedRefreshTokenAvailable.asStateFlow() @@ -63,12 +69,13 @@ internal class JwtProviderImpl @Inject constructor( runCatching { jwtDataStoreDataSource.loadTokens() }.onSuccess { tokens -> - this@JwtProviderImpl._cachedAccessToken = tokens.accessToken - this@JwtProviderImpl._cachedRefreshToken = tokens.refreshToken + _cachedAccessToken = tokens.accessToken + _cachedRefreshToken = tokens.refreshToken }.onFailure { exception -> - Log.e("JwtProvider", "Failed to persist tokens", exception) + Log.e("JwtProvider", "Failed to load tokens", exception) } - this.refreshTokenAbility() + + refreshTokenAbility() } override fun updateTokens(tokens: Tokens) { @@ -87,46 +94,45 @@ internal class JwtProviderImpl @Inject constructor( } } - override suspend fun resolveSession(): Boolean { + override suspend fun resolveSession(): Boolean = tokenMutex.withLock { val accessToken = _cachedAccessToken + if (accessToken != null && !accessToken.isExpired()) { refreshTokenAbility() - return true + return@withLock true } - reissueTokensLocked() + val reissued = reissueTokensLocked() refreshTokenAbility() - return checkIsAccessTokenAvailable() || checkIsRefreshTokenAvailable() + + reissued && checkIsAccessTokenAvailable() } - } - override suspend fun refreshSession(): Boolean { + override suspend fun refreshSession(): Boolean = tokenMutex.withLock { - reissueTokensLocked() + val reissued = reissueTokensLocked() refreshTokenAbility() - return checkIsAccessTokenAvailable() || checkIsRefreshTokenAvailable() + + reissued && checkIsAccessTokenAvailable() } - } private fun refreshTokenAbility() { - _isCachedAccessTokenAvailable.value = checkIsAccessTokenAvailable() - _isCachedRefreshTokenAvailable.value = checkIsRefreshTokenAvailable() + _isCachedAccessTokenAvailable.value = + checkIsAccessTokenAvailable() + _isCachedRefreshTokenAvailable.value = + checkIsRefreshTokenAvailable() } - private fun checkIsAccessTokenAvailable(): Boolean { - if (this._cachedAccessToken == null) { - return false - } - return !_cachedAccessToken!!.isExpired() - } + private fun checkIsAccessTokenAvailable(): Boolean = + _cachedAccessToken?.let { accessToken -> + !accessToken.isExpired() + } ?: false - private fun checkIsRefreshTokenAvailable(): Boolean { - if (this._cachedRefreshToken == null) { - return false - } - return !_cachedRefreshToken!!.isExpired() - } + private fun checkIsRefreshTokenAvailable(): Boolean = + _cachedRefreshToken?.let { refreshToken -> + !refreshToken.isExpired() + } ?: false private suspend fun reissueTokensLocked(): Boolean { val refreshToken = _cachedRefreshToken @@ -138,13 +144,20 @@ internal class JwtProviderImpl @Inject constructor( } return try { - val tokens = jwtReissueManager(refreshToken = refreshToken.value) + val tokens = jwtReissueManager( + refreshToken = refreshToken.value, + ) + updateTokensLocked(tokens = tokens) true } catch (exception: CannotReissueTokenException) { - if (exception.statusCode == 401 || exception.statusCode == 404) { + if ( + exception.statusCode == 401 || + exception.statusCode == 404 + ) { clearCachesLocked() } + false } } @@ -153,19 +166,27 @@ internal class JwtProviderImpl @Inject constructor( val previousAccessToken = _cachedAccessToken val previousRefreshToken = _cachedRefreshToken - this._cachedAccessToken = tokens.accessToken - this._cachedRefreshToken = tokens.refreshToken - this.refreshTokenAbility() + _cachedAccessToken = tokens.accessToken + _cachedRefreshToken = tokens.refreshToken + refreshTokenAbility() runCatchingCancellable { jwtDataStoreDataSource.storeTokens(tokens = tokens) }.onFailure { exception -> - this._cachedAccessToken = previousAccessToken - this._cachedRefreshToken = previousRefreshToken - this.refreshTokenAbility() + _cachedAccessToken = previousAccessToken + _cachedRefreshToken = previousRefreshToken + refreshTokenAbility() - Log.e("JwtProvider", "Failed to store tokens", exception) - throw IllegalStateException("Failed to persist tokens", exception) + Log.e( + "JwtProvider", + "Failed to store tokens", + exception, + ) + + throw IllegalStateException( + "Failed to persist tokens", + exception, + ) } } @@ -173,19 +194,27 @@ internal class JwtProviderImpl @Inject constructor( val previousAccessToken = _cachedAccessToken val previousRefreshToken = _cachedRefreshToken - this._cachedAccessToken = null - this._cachedRefreshToken = null - this.refreshTokenAbility() + _cachedAccessToken = null + _cachedRefreshToken = null + refreshTokenAbility() runCatchingCancellable { jwtDataStoreDataSource.clearTokens() }.onFailure { exception -> - this._cachedAccessToken = previousAccessToken - this._cachedRefreshToken = previousRefreshToken - this.refreshTokenAbility() + _cachedAccessToken = previousAccessToken + _cachedRefreshToken = previousRefreshToken + refreshTokenAbility() + + Log.e( + "JwtProvider", + "Failed to clear tokens", + exception, + ) - Log.e("JwtProvider", "Failed to clear tokens", exception) - throw IllegalStateException("Failed to clear persisted tokens", exception) + throw IllegalStateException( + "Failed to clear persisted tokens", + exception, + ) } } } diff --git a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/JwtReissueManager.kt b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/JwtReissueManager.kt index dff698ddb..07dc61816 100644 --- a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/JwtReissueManager.kt +++ b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/JwtReissueManager.kt @@ -10,6 +10,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody import okhttp3.logging.HttpLoggingInterceptor import team.aliens.dms.android.core.jwt.Tokens +import team.aliens.dms.android.core.jwt.di.TokenReissueUrl import team.aliens.dms.android.core.jwt.network.exception.CannotReissueTokenException import team.aliens.dms.android.core.jwt.network.model.TokensResponse import team.aliens.dms.android.core.jwt.toModel @@ -17,7 +18,7 @@ import team.aliens.dms.android.core.network.di.DefaultHttpLoggingInterceptor import javax.inject.Inject class JwtReissueManager @Inject constructor( - private val reissueUrl: String, + @TokenReissueUrl private val reissueUrl: String, @DefaultHttpLoggingInterceptor private val httpLoggingInterceptor: HttpLoggingInterceptor, baseHttpClient: OkHttpClient, ) { @@ -27,27 +28,40 @@ class JwtReissueManager @Inject constructor( }.build() } - suspend operator fun invoke(refreshToken: String): Tokens = withContext(Dispatchers.IO) { + suspend operator fun invoke( + refreshToken: String, + ): Tokens = withContext(Dispatchers.IO) { val request = buildTokenReissueRequest(refreshToken) - val response = client.newCall(request).execute() - if (response.isSuccessful) { - response.body.toTokensResponse().toModel() - } else { - throw CannotReissueTokenException(statusCode = response.code) + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body.toTokensResponse().toModel() + } else { + throw CannotReissueTokenException( + statusCode = response.code, + ) + } } } private fun ResponseBody?.toTokensResponse(): TokensResponse { requireNotNull(this) - return Gson().fromJson(this.string(), TokensResponse::class.java) + return Gson().fromJson(string(), TokensResponse::class.java) } - private fun buildTokenReissueRequest(refreshToken: String): Request = - Request.Builder().url(reissueUrl).put( - body = String().toRequestBody("application/json".toMediaType()), - ).addHeader( - name = "refresh-token", - value = refreshToken, - ).build() + private fun buildTokenReissueRequest( + refreshToken: String, + ): Request = + Request.Builder() + .url(reissueUrl) + .put( + body = String().toRequestBody( + "application/json".toMediaType(), + ), + ) + .addHeader( + name = "refresh-token", + value = refreshToken, + ) + .build() } diff --git a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/authenticator/JwtAuthenticator.kt b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/authenticator/JwtAuthenticator.kt index cf6e2b24e..6c5cdfc60 100644 --- a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/authenticator/JwtAuthenticator.kt +++ b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/authenticator/JwtAuthenticator.kt @@ -21,26 +21,48 @@ class JwtAuthenticator @Inject constructor( ): Request? { val request = response.request - if (request.shouldBeIgnored() || response.retryCount >= MAX_AUTH_RETRY_COUNT) { + if ( + request.shouldBeIgnored() || + response.retryCount >= MAX_AUTH_RETRY_COUNT + ) { return null } - val newAuthorization = refreshAuthorization() + val failedAuthorization = request.header(AUTHORIZATION_HEADER) + val newAuthorization = refreshAuthorization( + failedAuthorization = failedAuthorization, + ) return newAuthorization - ?.takeUnless { refreshedAuthorization -> - refreshedAuthorization == request.header(AUTHORIZATION_HEADER) + ?.takeUnless { authorization -> + authorization == failedAuthorization } - ?.let { refreshedAuthorization -> + ?.let { authorization -> request.newBuilder() - .header(AUTHORIZATION_HEADER, refreshedAuthorization) + .header(AUTHORIZATION_HEADER, authorization) .build() } } - private fun refreshAuthorization(): String? { + @Synchronized + private fun refreshAuthorization( + failedAuthorization: String?, + ): String? { + val currentAuthorization = runCatching { + "Bearer ${jwtProvider.cachedAccessToken.value}" + }.getOrNull() + + if ( + currentAuthorization != null && + currentAuthorization != failedAuthorization + ) { + return currentAuthorization + } + val refreshed = runCatching { - runBlocking { jwtProvider.refreshSession() } + runBlocking { + jwtProvider.refreshSession() + } }.getOrDefault(false) if (!refreshed) { @@ -52,12 +74,15 @@ class JwtAuthenticator @Inject constructor( }.getOrNull() } - private fun Request.shouldBeIgnored(): Boolean = ignoreRequests.requests.any { ignoreRequest -> - val path = this@shouldBeIgnored.url.encodedPath - val method = this@shouldBeIgnored.method.toHttpMethod() + private fun Request.shouldBeIgnored(): Boolean = + checkS3Request(url.toString()) || + ignoreRequests.requests.any { ignoreRequest -> + val path = url.encodedPath + val requestMethod = method.toHttpMethod() - path.contains(ignoreRequest.path) && method == ignoreRequest.method || checkS3Request(url = this@shouldBeIgnored.url.toString()) - } + path.contains(ignoreRequest.path) && + requestMethod == ignoreRequest.method + } private val Response.retryCount: Int get() { @@ -72,7 +97,8 @@ class JwtAuthenticator @Inject constructor( return count } - private fun checkS3Request(url: String): Boolean = url.contains(ResourceKeys.IMAGE_URL) + private fun checkS3Request(url: String): Boolean = + url.contains(ResourceKeys.IMAGE_URL) private companion object { const val AUTHORIZATION_HEADER = "authorization" diff --git a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt index f9f3c5271..f39b9a4cf 100644 --- a/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt +++ b/core/jwt/src/main/java/team/aliens/dms/android/core/jwt/network/interceptor/JwtInterceptor.kt @@ -39,11 +39,18 @@ class JwtInterceptor @Inject constructor( value = "Bearer ${jwtProvider.cachedAccessToken.value}", ) - private fun Request.shouldBeIgnored(): Boolean = ignoreRequests.requests.any { ignoreRequest -> - val path = this@shouldBeIgnored.url.encodedPath - val method = this@shouldBeIgnored.method.toHttpMethod() + private fun Request.shouldBeIgnored(): Boolean { + if (checkS3Request(url.toString())) { + return true + } - path.contains(ignoreRequest.path) && method == ignoreRequest.method || checkS3Request(url = this@shouldBeIgnored.url.toString()) + return ignoreRequests.requests.any { ignoreRequest -> + val path = url.encodedPath + val requestMethod = method.toHttpMethod() + + path.contains(ignoreRequest.path) && + requestMethod == ignoreRequest.method + } } private fun checkS3Request(url: String): Boolean {