113 lines
4.1 KiB
Kotlin
113 lines
4.1 KiB
Kotlin
package dev.inmo.postssystem.features.auth.client
|
|
|
|
import dev.inmo.postssystem.features.auth.common.*
|
|
import dev.inmo.postssystem.features.users.common.User
|
|
import dev.inmo.micro_utils.common.*
|
|
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
|
|
import io.ktor.client.HttpClientConfig
|
|
import io.ktor.client.features.cookies.*
|
|
import io.ktor.client.features.expectSuccess
|
|
import io.ktor.client.request.*
|
|
import io.ktor.client.statement.HttpReceivePipeline
|
|
import io.ktor.client.statement.HttpResponse
|
|
import io.ktor.http.*
|
|
import kotlinx.coroutines.*
|
|
import kotlinx.coroutines.sync.Mutex
|
|
import kotlinx.coroutines.sync.withLock
|
|
|
|
object AuthUnavailableException : Exception()
|
|
|
|
fun HttpClientConfig<*>.installClientAuthenticator(
|
|
baseUrl: String,
|
|
scope: CoroutineScope,
|
|
initialAuthKey: Either<AuthKey, AuthTokenInfo>,
|
|
onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit,
|
|
onUserRetrieved: suspend (User?) -> Unit,
|
|
onAuthKeyInvalidated: suspend () -> Unit
|
|
) {
|
|
// install(Logging) {
|
|
// logger = Logger.DEFAULT
|
|
// level = LogLevel.HEADERS
|
|
// }
|
|
install(HttpCookies) {
|
|
// Will keep an in-memory map with all the cookies from previous requests.
|
|
storage = AcceptAllCookiesStorage()
|
|
}
|
|
|
|
val authMutex = Mutex()
|
|
var currentRefreshToken: RefreshToken? = null
|
|
initialAuthKey.onFirst {
|
|
currentRefreshToken = it as? RefreshToken
|
|
}.onSecond {
|
|
currentRefreshToken = it.refresh
|
|
}
|
|
val creds = initialAuthKey.t1 as? AuthCreds
|
|
var userRefreshJob: Job? = null
|
|
|
|
install("Auth Token Refresher") {
|
|
val clientAuthFeature = ClientAuthFeature(this, baseUrl)
|
|
fun refreshUser(newTokenInfo: AuthTokenInfo) {
|
|
userRefreshJob ?.cancel()
|
|
userRefreshJob = scope.launchSafelyWithoutExceptions {
|
|
onUserRetrieved(clientAuthFeature.getMe(newTokenInfo.token))
|
|
}
|
|
}
|
|
initialAuthKey.onSecond { refreshUser(it) }
|
|
|
|
suspend fun refreshToken() {
|
|
val capturedRefresh = currentRefreshToken
|
|
|
|
runCatching {
|
|
when {
|
|
capturedRefresh == null && creds == null -> throw AuthUnavailableException
|
|
capturedRefresh != null -> {
|
|
currentRefreshToken = null
|
|
val newTokenInfo = clientAuthFeature.refresh(capturedRefresh)
|
|
currentRefreshToken = newTokenInfo ?.refresh
|
|
if (newTokenInfo == null) {
|
|
refreshToken()
|
|
} else {
|
|
onAuthKeyUpdated(newTokenInfo)
|
|
refreshUser(newTokenInfo)
|
|
}
|
|
}
|
|
creds != null -> {
|
|
val newAuthTokenInfo = clientAuthFeature.auth(creds)
|
|
|
|
if (newAuthTokenInfo != null) {
|
|
onAuthKeyUpdated(newAuthTokenInfo)
|
|
refreshUser(newAuthTokenInfo)
|
|
currentRefreshToken = newAuthTokenInfo.refresh
|
|
}
|
|
}
|
|
}
|
|
}.onFailure {
|
|
onAuthKeyInvalidated()
|
|
}
|
|
}
|
|
|
|
sendPipeline.intercept(HttpSendPipeline.State) {
|
|
if (!context.url.buildString().startsWith(baseUrl) || clientAuthFeature.isAuthRequest(context)) {
|
|
return@intercept
|
|
}
|
|
context.expectSuccess = false
|
|
if (authMutex.isLocked) {
|
|
authMutex.withLock { /* do nothing, just wait while mutex will be freed */ }
|
|
}
|
|
}
|
|
|
|
receivePipeline.intercept(HttpReceivePipeline.Before) {
|
|
if (
|
|
context.request.url.toString().startsWith(baseUrl)
|
|
&& context.response.status == HttpStatusCode.Unauthorized
|
|
) {
|
|
authMutex.withLock { refreshToken() }
|
|
val newResponse = context.client ?.request<HttpResponse>{
|
|
takeFrom(context.request)
|
|
} ?: return@intercept
|
|
proceedWith(newResponse)
|
|
}
|
|
}
|
|
}
|
|
}
|