Created
August 15, 2022 09:08
-
-
Save AlexGladkov/2ea7c9f3c8a31c7eadf7f42767d9acc0 to your computer and use it in GitHub Desktop.
Ktor feature token queue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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