Repository: ktorio/ktor
File: ktor-client/ktor-client-plugins/ktor-client-auth/common/src/io/ktor/client/plugins/auth/Auth.kt (Kotlin)

Code before:

        LOGGER.trace("Sending new request to ${call.request.url}")
        return proceed(request)
    }

    onRequest { request, _ ->
        providers.filter { it.sendWithoutRequest(request) }.forEach { provider ->
            LOGGER.trace("Adding auth headers for ${request.url} from provider $provider")
            val tokenVersion = tokenVersions.computeIfAbsent(provider) { AtomicCounter() }
            val requestTokenVersions = request.attributes
                .computeIfAbsent(tokenVersionsAttributeKey) { mutableMapOf() }
            requestTokenVersions[provider] = tokenVersion.atomic.value
            provider.addRequestHeaders(request)
        }
    }

    on(Send) { originalRequest ->
        val origin = proceed(originalRequest)
        if (origin.response.status !in pluginConfig.reAuthStatusCodes) return@on origin
        if (origin.request.attributes.contains(AuthCircuitBreaker)) return@on origin

        var call = origin

        val candidateProviders = HashSet(providers)

        while (call.response.status in pluginConfig.reAuthStatusCodes) {
            LOGGER.trace("Received 401 for ${call.request.url}")

            val (provider, authHeader) = findProvider(call, candidateProviders) ?: run {
                LOGGER.trace("Can not find auth provider for ${call.request.url}")
                return@on call
            }

            LOGGER.trace("Using provider $provider for ${call.request.url}")

            candidateProviders.remove(provider)
            if (!refreshTokenIfNeeded(call, provider, originalRequest)) return@on call
            call = executeWithNewToken(call, provider, originalRequest, authHeader)
        }
        return@on call
    }
}

/**
 * Install [Auth] plugin.
 */
public fun HttpClientConfig<*>.Auth(block: AuthConfig.() -> Unit) {
    install(Auth, block)
}

@PublishedApi

Review comment (bug):
Speaking of broken services, I've seen some APIs returning `200 OK` with actual status code in a body. We could provide a lambda instead of a list of statuses: 

```kotlin
var isUnauthorized: (HttpResponse) -> Boolean = { it.status == HttpStatusCode.Unauthorized }
````

@e5l, @bjhham, @marychatte, what do you think?

Code after:
        val requestTokenVersion = requestTokenVersions[provider]

        if (requestTokenVersion != null && requestTokenVersion >= tokenVersion.atomic.value) {
            LOGGER.trace { "Refreshing token for ${call.request.url}" }
            if (!provider.refreshToken(call.response)) {
                LOGGER.trace { "Refreshing token failed for ${call.request.url}" }
                return false
            } else {
                requestTokenVersions[provider] = tokenVersion.atomic.incrementAndGet()
            }
        }
        return true
    }

    suspend fun Send.Sender.executeWithNewToken(
        call: HttpClientCall,
        provider: AuthProvider,
        oldRequest: HttpRequestBuilder,
        authHeader: HttpAuthHeader?
    ): HttpClientCall {
        val request = HttpRequestBuilder()
        request.takeFromWithExecutionContext(oldRequest)
        provider.addRequestHeaders(request, authHeader)
        request.attributes.put(AuthCircuitBreaker, Unit)

        LOGGER.trace { "Sending new request to ${call.request.url}" }
        return proceed(request)
    }

    onRequest { request, _ ->
        providers.filter { it.sendWithoutRequest(request) }.forEach { provider ->
            LOGGER.trace { "Adding auth headers for ${request.url} from provider $provider" }
            val tokenVersion = tokenVersions.computeIfAbsent(provider) { AtomicCounter() }
            val requestTokenVersions = request.attributes
                .computeIfAbsent(tokenVersionsAttributeKey) { mutableMapOf() }
            requestTokenVersions[provider] = tokenVersion.atomic.value
            provider.addRequestHeaders(request)
        }
    }

    on(Send) { originalRequest ->
        val origin = proceed(originalRequest)
        if (!pluginConfig.isUnauthorizedResponse(origin.response)) return@on origin
        if (origin.request.attributes.contains(AuthCircuitBreaker)) return@on origin

        var call = origin

        val candidateProviders = HashSet(providers)

        while (pluginConfig.isUnauthorizedResponse(call.response)) {
            LOGGER.trace { "Unauthorized response for ${call.request.url}" }