Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new way #134

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package com.hoc081098.refreshtokensample.data.local

import java.io.IOException
import kotlinx.coroutines.flow.Flow

hoc081098 marked this conversation as resolved.
Show resolved Hide resolved
interface UserLocalSource {
fun user(): Flow<UserLocal?>

/**
* Updates the data transactionally in an atomic read-modify-write operation. All operations
* are serialized, and the [transform] itself is a coroutine so it can perform heavy work
* such as RPCs.
*
* The coroutine completes when the data has been persisted durably to disk (after which
* [user] will reflect the update). If the transform or write to disk fails, the
* transaction is aborted and an exception is thrown.
*
* @return the snapshot returned by the transform
* @throws IOException when an exception is encountered when writing data to disk
* @throws Exception when thrown by the transform function
*/
suspend fun update(transform: suspend (current: UserLocal?) -> UserLocal?): UserLocal?
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
Expand All @@ -25,70 +23,73 @@ class AuthInterceptor @Inject constructor(
private val apiService: Provider<ApiService>,
private val appDispatchers: AppDispatchers,
) : Interceptor {
private val mutex = Mutex()

override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request().also { debug("[1] $it") }
val request = chain.request().also { debug("[1] START url=${it.url}") }

if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) {
return chain.proceedWithToken(req, null)
if (NO_AUTH in request.headers.values(CUSTOM_HEADER)) {
return chain.proceedWithToken(request, null)
}

val token = runBlocking(appDispatchers.io) { userLocalSource.user().first() }
?.token
.also { debug("[2] $req $it") }
val res = chain.proceedWithToken(req, token)
.also { debug("[2] READ TOKEN token=${it.firstTwoCharactersAndLastTwoCharacters()}, url=${request.url}") }
val response = chain.proceedWithToken(request, token)

if (res.code != HTTP_UNAUTHORIZED || token == null) {
return res
if (response.code != HTTP_UNAUTHORIZED || token == null) {
return response
}

debug("[3] $req")
val newToken = executeTokenRefreshing(request, token)

return if (newToken !== null && newToken != token) {
chain.proceedWithToken(request, newToken)
} else {
response
}
}

private fun executeTokenRefreshing(request: Request, token: String?): String? {
val requestUrl = request.url
debug("[3] BEFORE REFRESHING token=${token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl")

val newToken: String? = runBlocking(appDispatchers.io) {
mutex.withLock {
val user =
userLocalSource.user().first().also { debug("[4] $req $it") }
val maybeUpdatedToken = user?.token
return runBlocking(appDispatchers.io) {
userLocalSource.update { currentUserLocal ->
val maybeUpdatedToken = currentUserLocal?.token
debug("[4] INSIDE REFRESHING maybeUpdatedToken=${maybeUpdatedToken.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl")

when {
user == null || maybeUpdatedToken == null -> null.also { debug("[5-1] $req") } // already logged out!
maybeUpdatedToken != token -> maybeUpdatedToken.also { debug("[5-2] $req") } // refreshed by another request
maybeUpdatedToken == null ->
null
.also { debug("[5-1] LOGGED OUT url=$requestUrl") } // already logged out!
maybeUpdatedToken != token ->
currentUserLocal
.also { debug("[5-2] REFRESHED BY ANOTHER url=$requestUrl") } // refreshed by another request
else -> {
debug("[5-3] $req")
debug("[5-3] START REFRESHING REQUEST url=$requestUrl")

val refreshTokenRes = apiService.get()
.refreshToken(user.toRefreshTokenBody())
.also { debug("[6] $req $it") }
val refreshTokenRes = apiService
.get()
.refreshToken(currentUserLocal.toRefreshTokenBody())

when (refreshTokenRes.code()) {
when (val code = refreshTokenRes.code()) {
HTTP_OK -> {
debug("[7-1] $req")
refreshTokenRes.body()!!.token.also { updatedToken ->
userLocalSource.update {
(it ?: return@update null)
.toBuilder()
.setToken(updatedToken)
.build()
}
}
currentUserLocal
.toBuilder()
.setToken(refreshTokenRes.body()!!.token)
.build()
.also { debug("[6-1] REFRESH SUCCESSFULLY newToken=${it.token.firstTwoCharactersAndLastTwoCharacters()}, url=$requestUrl") }
}
HTTP_UNAUTHORIZED -> {
debug("[7-2] $req")
userLocalSource.update { null }
null
HTTP_UNAUTHORIZED -> null.also {
debug("[6-2] REFRESH FAILED HTTP_UNAUTHORIZED url=$requestUrl")
}
else -> {
debug("[7-3] $req")
null
else -> currentUserLocal.also {
debug("[6-3] REFRESH FAILED code=$code, url=$requestUrl")
}
}
}
}
}
}?.token
}

return if (newToken !== null) chain.proceedWithToken(req, newToken) else res
}

private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response =
Expand All @@ -111,3 +112,9 @@ class AuthInterceptor @Inject constructor(
}

private fun UserLocal.toRefreshTokenBody() = RefreshTokenBody(refreshToken, username)

@Suppress("NOTHING_TO_INLINE")
private inline fun String?.firstTwoCharactersAndLastTwoCharacters(): String? {
this ?: return null
return if (length <= 4) this else "${take(2)}...${takeLast(2)}"
}