7

I am trying to use ktor client in Kotlin/MPP (Multiplatform) project and on JVM target feature basic authentication does not seem to have an effect.

Here is an example to reproduce:

import io.ktor.client.HttpClient
import io.ktor.client.features.ResponseException
import io.ktor.client.features.auth.Auth
import io.ktor.client.features.auth.providers.basic
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.DEFAULT
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logger
import io.ktor.client.features.logging.Logging
import io.ktor.client.request.get
import io.ktor.client.request.header
import kotlinx.coroutines.runBlocking
import java.util.*

fun main() = runBlocking {
    val client = HttpClient {
        install(Logging) {
            logger = Logger.DEFAULT
            level = LogLevel.HEADERS
        }
        install(JsonFeature) {
            serializer = KotlinxSerializer()
        }
        install(Auth) {
            basic {
                username = "user"
                password = "pass"
            }
        }
    }
    val url = "https://en.wikipedia.org/wiki/Main_Page"

    val failing = try {
        client.get<String>(url)
    } catch (e: ResponseException) {
        "failed"
    }

    val succeeding = try {
        client.get<String>(url) {
            header("Authorization", "Basic ${Base64.getEncoder().encodeToString("user:pass".toByteArray())}")
        }
    } catch (e: ResponseException) {
        "failed"
    }
}

Observation

From the logger output, you can see that client does not send Authorization header but I experience no problems when I provide such header manually:

First request (failing example:)

[main] INFO io.ktor.client.HttpClient - REQUEST: https://en.wikipedia.org/wiki/Main_Page
[main] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
[main] INFO io.ktor.client.HttpClient - COMMON HEADERS
[main] INFO io.ktor.client.HttpClient - -> Accept: application/json
[main] INFO io.ktor.client.HttpClient - -> Accept-Charset: UTF-8
[main] INFO io.ktor.client.HttpClient - CONTENT HEADERS

Second request (succeeding example:)

[main] INFO io.ktor.client.HttpClient - REQUEST: https://en.wikipedia.org/wiki/Main_Page
[main] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
[main] INFO io.ktor.client.HttpClient - COMMON HEADERS
[main] INFO io.ktor.client.HttpClient - -> Authorization: Basic dXNlcjpwYXNz
[main] INFO io.ktor.client.HttpClient - -> Accept: application/json
[main] INFO io.ktor.client.HttpClient - -> Accept-Charset: UTF-8
[main] INFO io.ktor.client.HttpClient - CONTENT HEADERS

Environment

  • Kotlin: 1.4-M1

Ktor Artifacts version 1.3.1:

  • ktor-client-core
  • ktor-client-logging
  • ktor-client-json
  • ktor-client-serialization
  • ktor-client-auth-basic

Did I miss something?

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
kuza
  • 2,761
  • 3
  • 22
  • 56
  • Not sure if this is the case, but that was a fixed bug. Try using ktor version 1.3.5-M1 – andylamax Apr 24 '20 at 13:02
  • I cannot find such a version. Can you suggest artifacts‘ sources? – kuza Apr 24 '20 at 17:10
  • Forgive my memory, I confused coroutines version and ktor version. You should use ktor version 1.3.2-1.4-M1. Because it is the one compiled with the new backend. 1.3.1 uses the old backend – andylamax Apr 25 '20 at 23:53

2 Answers2

13

Please add sendWithoutRequest = true

1.x https://api.ktor.io/1.3.1/io.ktor.client.features.auth.providers/-basic-auth-config/send-without-request.html

install(Auth) {
    basic {
        sendWithoutRequest = true
        username = "user"
        password = "pass"
    }
}

2.x https://ktor.io/docs/basic-client.html#configure

install(Auth) {
    basic {
        sendWithoutRequest { true }
        credentials {
            BasicAuthCredentials(
                username = "user",
                password = "pass",
            )
        }
    }
}

Result:

sending with sendWithoutRequest set to true
[main] INFO io.ktor.client.HttpClient - REQUEST: https://en.wikipedia.org/wiki/Main_Page
[main] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
[main] INFO io.ktor.client.HttpClient - COMMON HEADERS
[main] INFO io.ktor.client.HttpClient - -> Authorization: Basic dXNlcjpwYXNz
[main] INFO io.ktor.client.HttpClient - -> Accept: application/json
[main] INFO io.ktor.client.HttpClient - -> Accept-Charset: UTF-8
[main] INFO io.ktor.client.HttpClient - CONTENT HEADERS

Explanation:

By default, Ktor will wait for the server to respond with 401, Unauthorized, and only then send the authentication header. In your example, wiki never responds with a 401, as it is not a protected resource. Therefore, adding sendWithoutRequest is required. If you tried with some url that does respond with a 401, you would see that Ktor will then send a second request (after receiving 401) with the authentication header. You can try with this url to see - https://api.sumologic.com/api/v1/collectors.

This is the logging when done against that protected api with sendWithoutRequest turned off, your original input. As you can see, there are now 2 requests made, the first without the authorization header, and then the second one, with the authorization header, after the server has responded with a 401.

sending with sendWithoutRequest set to false and hitting a protected resource
    [main] INFO io.ktor.client.HttpClient - REQUEST: https://api.sumologic.com/api/v1/collectors
    [main] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
    [main] INFO io.ktor.client.HttpClient - COMMON HEADERS
    [main] INFO io.ktor.client.HttpClient - -> Accept: application/json
    [main] INFO io.ktor.client.HttpClient - -> Accept-Charset: UTF-8
    [main] INFO io.ktor.client.HttpClient - CONTENT HEADERS
    [main] INFO io.ktor.client.HttpClient - REQUEST: https://api.sumologic.com/api/v1/collectors
    [main] INFO io.ktor.client.HttpClient - METHOD: HttpMethod(value=GET)
    [main] INFO io.ktor.client.HttpClient - COMMON HEADERS
    [main] INFO io.ktor.client.HttpClient - -> Accept: application/json
    [main] INFO io.ktor.client.HttpClient - -> Accept-Charset: UTF-8
    [main] INFO io.ktor.client.HttpClient - -> Authorization: Basic dXNlcjpwYXNz
    [main] INFO io.ktor.client.HttpClient - CONTENT HEADERS

Note: I just saw a comment by Andylamax that a new version "fixes" it. Perhaps, I don't know as I haven' tried with that new version. But I would like to add that this is not something unique to Ktor, and at least in this respect is not a bug (but maybe they changed their minds? Again, I don't know). In fact, it is my experience with C# that led me to suspect what's going in here and find the answer. The WebRequest in C# behaves the same way, you need to set PreAuthenticate to true to send the credentials immediately. See here https://learn.microsoft.com/en-us/dotnet/api/system.net.webrequest.preauthenticate?view=netcore-3.1.

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
Dmitri
  • 2,563
  • 1
  • 22
  • 30
  • Aha! I knew I was missing something. Perhaps Ktor online documentation should mention that. But odd thing, even though I used Wikipedia URL in the question I did use endpoint returning HTTP 401 in practice and Ktor immediately throws `ClientRequestException` on the first request without retiring with authorization. Perhaps something else is missing _(I‘d like to know from you if you can share more.)_ Either way, I have to carry authorization header on each request so your answer solves the issue for sure. – kuza Apr 26 '20 at 17:16
  • Same for Bearer token. It should look like this `sendWithoutRequest { request -> // return false for urls which don't need an Authorization header (Ex: token or refresh token urls), otherwise return true to include the Authorization header }` – vovahost Nov 04 '22 at 05:54
0

Ktor 2.1.0

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import io.ktor.client.statement.*

private val httpClient = HttpClient(CIO) {
    install(Auth) {
        basic {
            credentials {
                BasicAuthCredentials(
                        username = "user",
                        password = "pass"
                )
            }
        }
    }
}

build.gradle

implementation("io.ktor:ktor-client-core:2.1.0")
implementation("io.ktor:ktor-client-cio:2.1.0")
implementation("io.ktor:ktor-client-auth:2.1.0")
tomrozb
  • 25,773
  • 31
  • 101
  • 122