aboutsummaryrefslograw-logtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--changes-entries/digest-authinfo-errors.txt[diff] [file]3
-rw-r--r--modules/aaa/mod_auth_digest.c[diff] [file]13
-rw-r--r--test/modules/aaa/test_011_authinfo_errors.py[diff] [file]87
3 files changed, 102 insertions, 1 deletions
diff --git a/changes-entries/digest-authinfo-errors.txt b/changes-entries/digest-authinfo-errors.txt
new file mode 100644
index 0000000000..7d5ba783ea
--- /dev/null
+++ b/changes-entries/digest-authinfo-errors.txt
@@ -0,0 +1,3 @@
+ *) mod_auth_digest: With one-time nonces (AuthDigestNonceLifetime 0),
+ stop needlessly re-challenging a client after an error response.
+ [Joe Orton]
diff --git a/modules/aaa/mod_auth_digest.c b/modules/aaa/mod_auth_digest.c
index 2e59b7a397..c08ffff403 100644
--- a/modules/aaa/mod_auth_digest.c
+++ b/modules/aaa/mod_auth_digest.c
@@ -1297,6 +1297,14 @@ static int note_digest_auth_failure(request_rec *r,
domain = conf->uri_list;
}
+ /* A challenge supersedes any Authentication-Info added at fixups
+ * for this request, e.g. when a handler notes an auth failure
+ * after the user was authenticated: the client must not be given
+ * both a nextnonce and a new nonce. */
+ apr_table_unset(r->err_headers_out,
+ (PROXYREQ_PROXY == r->proxyreq)
+ ? "Proxy-Authentication-Info" : "Authentication-Info");
+
apr_table_mergen(r->err_headers_out,
(PROXYREQ_PROXY == r->proxyreq)
? "Proxy-Authenticate" : "WWW-Authenticate",
@@ -1878,7 +1886,10 @@ static int add_auth_info(request_rec *r)
}
if (ai && ai[0]) {
- apr_table_mergen(r->headers_out,
+ /* This must use ->err_headers_out so it survives an error
+ * response, else a one-time-nonce client loses the nextnonce
+ * and is stale-challenged on its next request. */
+ apr_table_mergen(r->err_headers_out,
(PROXYREQ_PROXY == r->proxyreq)
? "Proxy-Authentication-Info"
: "Authentication-Info",
diff --git a/test/modules/aaa/test_011_authinfo_errors.py b/test/modules/aaa/test_011_authinfo_errors.py
new file mode 100644
index 0000000000..64361f53ba
--- /dev/null
+++ b/test/modules/aaa/test_011_authinfo_errors.py
@@ -0,0 +1,87 @@
+"""Authentication-Info survives an error response.
+
+The header is added at fixups, so unless it goes into err_headers_out it
+is dropped when the response turns into an error, costing a one-time-nonce
+client the nextnonce and forcing a stale re-challenge. A 404 for a missing
+file inside the protected area reaches the error path while fully
+authenticated, so it exercises this without a handler like mod_dav.
+"""
+
+from . import digest_client as dc
+from .env import AAATestEnv
+
+
+class TestAuthInfoOnError:
+
+ def url(self, env, location, path):
+ return env.mkurl("http", "aaa", f"/digest/{location}/{path}")
+
+ def uri(self, location, path):
+ return f"/digest/{location}/{path}"
+
+ def challenge(self, env, location, path="secret.txt"):
+ r = env.curl_get(self.url(env, location, path))
+ assert r.response["status"] == 401
+ return dc.DigestChallenge.parse(r.response["header"]["www-authenticate"])
+
+ def auth(self, env, location, path, challenge, nc="00000001"):
+ return dc.build_authorization(
+ AAATestEnv.DIGEST_USER, challenge, AAATestEnv.DIGEST_PASSWORD,
+ method="GET", uri=self.uri(location, path), nc=nc,
+ cnonce="authinfo-cnonce")
+
+ def get(self, env, location, path, challenge, nc="00000001"):
+ return env.curl_get(self.url(env, location, path), options=[
+ "-H", f"Authorization: {self.auth(env, location, path, challenge, nc)}"])
+
+ def test_digest_110_error_response_carries_authentication_info(self, env):
+ # A 404 for an authenticated request still confirms the response to
+ # the client: Authentication-Info with a valid rspauth.
+ location = "default"
+ challenge = self.challenge(env, location)
+ r = self.get(env, location, "no-such-file.txt", challenge)
+ assert r.response["status"] == 404
+ assert "authentication-info" in r.response["header"], \
+ "the error response dropped Authentication-Info"
+ ai = dc.parse_params(r.response["header"]["authentication-info"])
+ expect = dc.rspauth_digest(
+ dc.ha1(AAATestEnv.DIGEST_USER, AAATestEnv.REALM,
+ AAATestEnv.DIGEST_PASSWORD),
+ challenge.nonce, "00000001", "authinfo-cnonce", "auth",
+ self.uri(location, "no-such-file.txt"))
+ assert ai.get("rspauth") == expect, "rspauth wrong on the error response"
+
+ def test_digest_111_onetime_client_continues_after_an_error(self, env):
+ # Under one-time nonces the error must still hand back a nextnonce,
+ # or the client's next request is stale-challenged. Prove the client
+ # can carry straight on to a real request with what the 404 gave it.
+ location = "onetime"
+ challenge = self.challenge(env, location)
+ r = self.get(env, location, "no-such-file.txt", challenge)
+ assert r.response["status"] == 404
+ assert "authentication-info" in r.response["header"], \
+ "the one-time-nonce error response dropped Authentication-Info"
+ ai = dc.parse_params(r.response["header"]["authentication-info"])
+ assert "nextnonce" in ai, "no nextnonce to continue with after the error"
+
+ challenge.nonce = ai["nextnonce"]
+ ok = self.get(env, location, "secret.txt", challenge)
+ assert ok.response["status"] == 200, \
+ "the nextnonce from the error response was not usable"
+
+ def test_digest_112_challenge_has_no_authentication_info(self, env):
+ # When the error is itself a 401, the challenge stands alone: the
+ # client is not handed a nextnonce it could not use beside a fresh
+ # nonce. A wrong password reaches note_digest_auth_failure.
+ location = "onetime"
+ challenge = self.challenge(env, location)
+ bad = dc.build_authorization(
+ AAATestEnv.DIGEST_USER, challenge, "wrong-password",
+ method="GET", uri=self.uri(location, "secret.txt"),
+ cnonce="authinfo-cnonce")
+ r = env.curl_get(self.url(env, location, "secret.txt"),
+ options=["-H", f"Authorization: {bad}"])
+ env.httpd_error_log.ignore_recent(lognos=["AH01794"]) # password mismatch
+ assert r.response["status"] == 401
+ assert "authentication-info" not in r.response["header"], \
+ "a 401 challenge carried an Authentication-Info header"
This site is maintained by Jamie Landeg-Jones <jamie@catflap.org>, and is not an official FreeBSD project, nor is it endorsed by the FreeBSD team.