Implement token invalidation

This commit is contained in:
Kirill Kamakin
2022-04-05 18:29:09 +05:00
parent 57f4ec4e22
commit 76a49a41a1
10 changed files with 91 additions and 86 deletions

View File

@@ -13,4 +13,6 @@ interface AuthRepo {
suspend fun requireAuthHeader(): String
suspend fun logout()
fun invalidateAuthHeader(header: String)
}

View File

@@ -37,12 +37,14 @@ class AuthRepoImpl @Inject constructor(
}.getOrThrow() // Throw error to show it to user
}
override suspend fun getAuthHeader(): String? {
override suspend fun getAuthHeader(): String? = runCatchingExceptCancel {
Timber.v("getAuthHeader() called")
return currentAccount()
currentAccount()
?.let { getAuthToken(it) }
?.let { AUTH_HEADER_FORMAT.format(it) }
}
}.onFailure {
Timber.e(it, "getAuthHeader: can't request auth header")
}.getOrNull()
private suspend fun getAuthToken(account: Account?): String? {
return account?.let { accountManagerInteractor.getAuthToken(it) }
@@ -67,6 +69,16 @@ class AuthRepoImpl @Inject constructor(
accountManagerInteractor.removeAccount(account)
}
override fun invalidateAuthHeader(header: String) {
Timber.v("invalidateAuthHeader() called with: header = $header")
val token = header.substringAfter("Bearer ")
if (token == header) {
Timber.w("invalidateAuthHeader: can't find token in $header")
} else {
accountManagerInteractor.invalidateAuthToken(token)
}
}
companion object {
private const val AUTH_HEADER_FORMAT = "Bearer %s"
}

View File

@@ -0,0 +1,41 @@
package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.auth.AuthRepo
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthenticationInterceptor @Inject constructor(
private val authRepo: AuthRepo,
) : Interceptor {
private val authHeader: String?
get() = runBlocking { authRepo.getAuthHeader() }
override fun intercept(chain: Interceptor.Chain): Response {
val currentHeader = authHeader ?: return chain.proceed(chain.request())
val response = proceedWithAuthHeader(chain, currentHeader)
if (listOf(401, 403).contains(response.code)) {
authRepo.invalidateAuthHeader(currentHeader)
}
val newHeader = authHeader ?: return response
return proceedWithAuthHeader(chain, newHeader)
}
private fun proceedWithAuthHeader(
chain: Interceptor.Chain,
authHeader: String,
) = chain.proceed(
chain.request()
.newBuilder()
.header(HEADER_NAME, authHeader)
.build()
)
companion object {
private const val HEADER_NAME = "Authorization"
}
}

View File

@@ -5,17 +5,17 @@ import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject
class OkHttpBuilder
@Inject
constructor(
class OkHttpBuilder @Inject constructor(
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
private val interceptors: Set<@JvmSuppressWildcards Interceptor>
private val authenticationInterceptor: AuthenticationInterceptor,
private val interceptors: Set<@JvmSuppressWildcards Interceptor>,
) {
fun buildOkHttp(): OkHttpClient {
Timber.v("buildOkHttp() called")
return OkHttpClient.Builder()
.apply { for (interceptor in interceptors) addNetworkInterceptor(interceptor) }
.build()
return OkHttpClient.Builder().apply {
addInterceptor(authenticationInterceptor)
for (interceptor in interceptors) addNetworkInterceptor(interceptor)
}.build()
}
}

View File

@@ -1,6 +1,5 @@
package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
@@ -10,20 +9,19 @@ import javax.inject.Singleton
@Singleton
class RecipeDataSourceImpl @Inject constructor(
private val authRepo: AuthRepo,
private val recipeServiceFactory: ServiceFactory<RecipeService>,
) : RecipeDataSource {
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
Timber.v("requestRecipes() called with: start = $start, limit = $limit")
val recipeSummary = getRecipeService().getRecipeSummary(start, limit, getToken())
val recipeSummary = getRecipeService().getRecipeSummary(start, limit)
Timber.v("requestRecipes() returned: $recipeSummary")
return recipeSummary
}
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
Timber.v("requestRecipeInfo() called with: slug = $slug")
val recipeInfo = getRecipeService().getRecipe(slug, getToken())
val recipeInfo = getRecipeService().getRecipe(slug)
Timber.v("requestRecipeInfo() returned: $recipeInfo")
return recipeInfo
}
@@ -32,6 +30,4 @@ class RecipeDataSourceImpl @Inject constructor(
Timber.v("getRecipeService() called")
return recipeServiceFactory.provideService()
}
private suspend fun getToken(): String? = authRepo.getAuthHeader()
}

View File

@@ -3,7 +3,6 @@ package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query
@@ -12,12 +11,10 @@ interface RecipeService {
suspend fun getRecipeSummary(
@Query("start") start: Int,
@Query("limit") limit: Int,
@Header("Authorization") authHeader: String?,
): List<GetRecipeSummaryResponse>
@GET("/api/recipes/{recipe_slug}")
suspend fun getRecipe(
@Path("recipe_slug") recipeSlug: String,
@Header("Authorization") authHeader: String?,
): GetRecipeResponse
}