Fix three basic-auth regressions surfaced by end-to-end testing

- InstanceDetector: a single 401 from one probe was over-eagerly concluded
  as "credentials invalid" / "credentials required". On instances behind a
  reverse proxy where one probe path (e.g. Yattee Server's /info) hits a
  same-origin redirect, iOS URLSession strips the Authorization header on
  the redirect and the request 401s even with valid credentials. Track 401s
  across all probes and only conclude basicAuthRequired/basicAuthInvalid
  when no probe matched and at least one returned 401.

- InstanceLoginView: the Invidious/Piped login flow constructed an API
  client backed by the shared appEnvironment.httpClient, which has no
  per-instance basic-auth headers. For instances behind a reverse proxy,
  the login POST 401d before reaching the upstream login endpoint. Build a
  per-instance HTTPClient with the basic-auth Authorization header baked in
  via setDefaultHeaders, mirroring ContentService.httpClientWithBasicAuth.

- InvidiousAPI.login: the login function constructs its own URLSession (to
  capture Set-Cookie via a redirect-blocking delegate), so it never
  inherits headers from the injected httpClient. Add an optional
  extraHeaders parameter and have InstanceLoginView pass the basic-auth
  header through when present. PipedAPI.login uses httpClient.fetch and
  inherits defaultHeaders correctly, so no change is needed there.
This commit is contained in:
Arkadiusz Fal
2026-04-06 22:11:55 +02:00
parent 3dd4073db7
commit eefd49f743
3 changed files with 54 additions and 12 deletions

View File

@@ -177,13 +177,33 @@ struct InstanceLoginView: View {
/// Performs the login based on instance type.
/// - Returns: The credential (SID for Invidious, token for Piped)
private func performLogin(appEnvironment: AppEnvironment) async throws -> String {
// If the instance sits behind an HTTP Basic Auth reverse proxy, the login
// POST must carry that proxy's Authorization header too otherwise the
// request 401s before reaching the upstream login endpoint. Bake the
// header into a fresh per-instance HTTPClient.
let basicAuthHeader = appEnvironment.basicAuthCredentialsManager.basicAuthHeader(for: instance)
let extraHeaders: [String: String]? = basicAuthHeader.map { ["Authorization": $0] }
let httpClient: HTTPClient
if let basicAuthHeader {
httpClient = appEnvironment.httpClientFactory.createClient(for: instance)
await httpClient.setDefaultHeaders(["Authorization": basicAuthHeader])
} else {
httpClient = appEnvironment.httpClient
}
switch instance.type {
case .invidious:
let api = InvidiousAPI(httpClient: appEnvironment.httpClient)
return try await api.login(email: username, password: password, instance: instance)
// InvidiousAPI.login uses its own URLSession (to handle redirect/Set-Cookie),
// so it doesn't inherit defaultHeaders from the injected HTTPClient. Pass
// the basic-auth header explicitly so the login POST passes the proxy.
let api = InvidiousAPI(httpClient: httpClient)
return try await api.login(email: username, password: password, instance: instance, extraHeaders: extraHeaders)
case .piped:
let api = PipedAPI(httpClient: appEnvironment.httpClient)
// PipedAPI.login uses httpClient.fetch which DOES inherit defaultHeaders,
// so the basic-auth header on the per-instance client is sufficient.
let api = PipedAPI(httpClient: httpClient)
return try await api.login(username: username, password: password, instance: instance)
default: