Skip to content

Instantly share code, notes, and snippets.

@AlexGladkov
Created August 15, 2022 09:08
Show Gist options
  • Save AlexGladkov/2ea7c9f3c8a31c7eadf7f42767d9acc0 to your computer and use it in GitHub Desktop.
Save AlexGladkov/2ea7c9f3c8a31c7eadf7f42767d9acc0 to your computer and use it in GitHub Desktop.
Ktor feature token queue
internal class AuthRefreshFeature(
private val localAuthDataSource: LocalAuthDataSource,
private val refreshTokenDataSource: KtorRefreshTokenDataSource,
private val localAuthErrorDataSource: LocalAuthErrorDataSource,
private val json: Json
) {
class Config(
var localAuthDataSource: LocalAuthDataSource? = null,
var refreshTokenDataSource: KtorRefreshTokenDataSource? = null,
var localAuthErrorDataSource: LocalAuthErrorDataSource? = null,
var json: Json? = null,
var maxAttempts: Int = 1,
var platformConfigure: Configuration? = null
)
companion object Feature : HttpClientFeature<Config, AuthRefreshFeature> {
const val ACCESS_TOKEN_HEADER = "access-token"
private val refreshTokenMutex = Mutex()
private val refreshTokenProcessStarted = AtomicBoolean(false)
override val key: AttributeKey<AuthRefreshFeature> = AttributeKey("AuthRefreshFeature")
override fun prepare(block: Config.() -> Unit): AuthRefreshFeature {
val config = Config().apply(block)
return AuthRefreshFeature(
requireNotNull(config.localAuthDataSource),
requireNotNull(config.refreshTokenDataSource),
requireNotNull(config.localAuthErrorDataSource),
requireNotNull(config.json),
)
}
@OptIn(InternalAPI::class)
override fun install(feature: AuthRefreshFeature, scope: HttpClient) {
scope.feature(HttpSend)?.intercept { call, context ->
if (call.response.status == HttpStatusCode.Unauthorized ||
call.response.status == HttpStatusCode.Conflict
) {
val authError = parseAuthError(
response = call.response,
json = feature.json
)
if (authError is AuthError.InvalidRefreshToken) {
feature.localAuthDataSource.setAccessToken("")
feature.localAuthDataSource.setRefreshToken("")
}
if (authError is AuthError.InvalidAccessToken) {
refreshTokenMutex.lock()
val refreshToken = feature.localAuthDataSource.getRefreshToken()
if (refreshToken.isNullOrEmpty()) {
refreshTokenMutex.unlock()
throw NoRefreshTokenError()
}
try {
if (!refreshTokenProcessStarted.get()) {
val response = feature.refreshTokenDataSource.refreshToken(
refreshToken = refreshToken,
httpClient = scope
)
feature.localAuthDataSource.setAccessToken(response.authToken)
feature.localAuthDataSource.setRefreshToken(response.refreshToken)
refreshTokenProcessStarted.set(true)
}
refreshTokenMutex.unlock()
val request = HttpRequestBuilder().apply {
takeFromWithExecutionContext(context)
headers.remove(ACCESS_TOKEN_HEADER)
header(ACCESS_TOKEN_HEADER, feature.localAuthDataSource.getAccessToken())
}
execute(request)
} catch (requestException: ClientRequestException) {
refreshTokenMutex.unlock()
refreshTokenProcessStarted.set(false)
feature.localAuthDataSource.setRefreshToken("")
feature.localAuthDataSource.setAccessToken("")
val refreshTokenAuthError = parseAuthError(
response = requestException.response,
json = feature.json
)
if (refreshTokenAuthError is AuthError) {
feature.localAuthErrorDataSource.emmitError(refreshTokenAuthError)
}
throw refreshTokenAuthError
}
} else {
refreshTokenMutex.unlock()
refreshTokenProcessStarted.set(false)
if (authError is AuthError) {
feature.localAuthErrorDataSource.emmitError(authError)
}
throw authError
}
} else {
refreshTokenProcessStarted.set(false)
call
}
}
}
private suspend fun parseAuthError(response: HttpResponse, json: Json): Exception {
return when (val apiError = response.readApiErrorModel(json)) {
is BadResponse.ApiError -> {
when {
apiError.hasRequestError ->
when {
apiError.hasAccessTokenErrorMessage -> AuthError.InvalidAccessToken
apiError.hasRefreshTokenErrorMessage -> AuthError.InvalidRefreshToken
apiError.hasRefreshTokenIsNotSpecifiedErrorMessage -> AuthError.RefreshTokenIsNotSpecified
else -> AuthError.UnknownAuthError(apiError.originalResponseText)
}
apiError.hasOAuthError -> AuthError.OAuthServiceUnavailable
else -> AuthError.UnknownAuthError(apiError.originalResponseText)
}
}
is BadResponse.Unknown -> apiError
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment