sync: do not cache a challenge we refuse to answer

The cleartext gate sits on the emission, but the 401 branch cached the
Basic challenge before reaching it. So after refusing a plain-HTTP
challenge the handler still believed Basic was in play: the preemptive
block is skipped because it requires both caches empty, the refusal
repeats, and the 401 after that logs "Basic credentials didn't work last
time" about a credential that never reached the wire.
This commit is contained in:
2026-09-07 21:45:44 +02:00
parent 9f832686fb
commit 89c3a5f66e
3 changed files with 28 additions and 1 deletions
@@ -116,7 +116,12 @@ class BasicDigestAuthHandler(
}
}
basicAuth = newBasicAuth
// ⚠️ Not cached if we would refuse to answer it. Caching a challenge
// we never answer leaves the handler believing Basic is in play: the
// preemptive block is skipped, the refusal repeats, and the 401 after
// that reports "Basic credentials didn't work last time" about a
// credential that was never put on the wire.
basicAuth = newBasicAuth?.takeIf { request.isHttps || insecureBasic }
digestAuth = newDigestAuth
}
@@ -60,6 +60,23 @@ class BasicDigestAuthHandlerTest {
assertNull(authenticator.authenticateRequest(original, response))
}
@Test
fun aRefusedCleartextChallengeIsNotCached() {
val authenticator = BasicDigestAuthHandler(null, "user", "password")
val original = Request.Builder().url("http://example.com").build()
val response = Builder()
.request(original)
.protocol(Protocol.HTTP_1_1)
.code(401).message("Authentication required")
.header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
.build()
assertNull(authenticator.authenticateRequest(original, response))
// Caching it would make the *next* 401 report that the credentials did
// not work, about a credential that was never sent.
assertNull(authenticator.authenticateRequest(original, response))
}
@Test
fun cleartextBasicIsSentWhenExplicitlyAllowed() {
val authenticator = BasicDigestAuthHandler(null, "user", "password", insecureBasic = true)