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
}

View File

@@ -21,6 +21,11 @@ class AccountAuthenticatorImpl @Inject constructor(
private val accountManager: AccountManager,
) : AbstractAccountAuthenticator(context) {
private val accountType: String
get() = accountParameters.accountType
private val authTokenType: String
get() = accountParameters.authTokenType
override fun getAuthToken(
response: AccountAuthenticatorResponse,
account: Account,
@@ -50,7 +55,7 @@ class AccountAuthenticatorImpl @Inject constructor(
return Bundle().apply {
putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
putString(AccountManager.KEY_ACCOUNT_TYPE, accountParameters.accountType)
putString(AccountManager.KEY_ACCOUNT_TYPE, accountType)
putString(AccountManager.KEY_AUTHTOKEN, token)
}
}
@@ -114,13 +119,13 @@ class AccountAuthenticatorImpl @Inject constructor(
// end region
private fun checkAccountType(accountType: String) {
if (accountType != accountParameters.accountType) {
if (accountType != this.accountType) {
throw UnsupportedAccountType(accountType)
}
}
private fun checkAuthTokenType(authTokenType: String) {
if (authTokenType != accountParameters.authTokenType) {
if (authTokenType != this.authTokenType) {
throw UnsupportedAuthTokenType(authTokenType)
}
}

View File

@@ -14,4 +14,6 @@ interface AccountManagerInteractor {
fun accountUpdatesFlow(): Flow<Array<Account>>
suspend fun removeAccount(account: Account)
fun invalidateAuthToken(token: String)
}

View File

@@ -13,16 +13,21 @@ class AccountManagerInteractorImpl @Inject constructor(
private val accountParameters: AccountParameters,
) : AccountManagerInteractor {
private val accountType: String
get() = accountParameters.accountType
private val authTokenType: String
get() = accountParameters.authTokenType
override fun getAccounts(): Array<Account> {
Timber.v("getAccounts() called")
val accounts = accountManager.getAccountsByType(accountParameters.accountType)
val accounts = accountManager.getAccountsByType(accountType)
Timber.v("getAccounts() returned: ${accounts.contentToString()}")
return accounts
}
override suspend fun addAccount(email: String, password: String): Account {
Timber.v("addAccount() called with: email = $email, password = $password")
val account = Account(email, accountParameters.accountType)
val account = Account(email, accountType)
removeAccount(account) // Remove account if it was created earlier
accountManager.addAccountExplicitly(account, password, null)
return account
@@ -32,7 +37,7 @@ class AccountManagerInteractorImpl @Inject constructor(
Timber.v("getAuthToken() called with: account = $account")
val bundle = accountManager.getAuthToken(
account,
accountParameters.authTokenType,
authTokenType,
null,
null,
null,
@@ -49,7 +54,7 @@ class AccountManagerInteractorImpl @Inject constructor(
override fun accountUpdatesFlow(): Flow<Array<Account>> {
Timber.v("accountUpdatesFlow() called")
return accountManager.accountUpdatesFlow(accountParameters.accountType)
return accountManager.accountUpdatesFlow(accountType)
}
override suspend fun removeAccount(account: Account) {
@@ -57,4 +62,9 @@ class AccountManagerInteractorImpl @Inject constructor(
val bundle = accountManager.removeAccount(account, null, null, null).await()
Timber.d("removeAccount: result is ${bundle.result()}")
}
override fun invalidateAuthToken(token: String) {
Timber.v("resetAuthToken() called with: token = $token")
accountManager.invalidateAuthToken(accountType, token)
}
}