From 194789e78bfd1d0b49b82ebba68a2e062d744249 Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Fri, 23 Oct 2026 15:22:52 -0700 Subject: [PATCH] gh-60299: Fix ProxyDigestAuthHandler credential lookup to use proxy authority AbstractDigestAuthHandler.get_authorization() always looked up credentials using the target request URL (req.full_url), even when invoked via ProxyDigestAuthHandler, whose credentials are registered against the proxy's host:port. The lookup never matched, no Proxy-Authorization header was produced, and proxy digest auth failed with an unhandled 407. Co-Authored-By: Claude Sonnet 5 Fix urllib2_localnet test cases. --- Lib/test/test_urllib2.py | 60 +++++++++++++++++++ Lib/test/test_urllib2_localnet.py | 8 ++- Lib/urllib/request.py | 8 ++- ...6-10-23-21-52-40.gh-issue-60299.Af6IzL.rst | 4 ++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-23-21-52-40.gh-issue-60299.Af6IzL.rst diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 7efbc81a16096a..226499012faa4b 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1735,6 +1735,66 @@ def test_proxy_basic_auth(self): "proxy.example.com:3128", ) + def test_proxy_digest_auth(self): + # gh-60299: ProxyDigestAuthHandler must look up credentials using + # the proxy's authority + opener = OpenerDirector() + ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128")) + opener.add_handler(ph) + password_manager = MockPasswordManager() + digest_handler = urllib.request.ProxyDigestAuthHandler(password_manager) + realm = "ACME Networks" + http_handler = MockHTTPHandlerRedirect( + 407, + 'Proxy-Authenticate: Digest realm="%s", nonce="abcdef0123456789", ' + 'qop="auth"\r\n\r\n' % realm) + opener.add_handler(digest_handler) + opener.add_handler(http_handler) + + user, password = "wile", "coyote" + request_url = "http://acme.example.com/protected" + proxy_authority = "proxy.example.com:3128" + + digest_handler.add_password(realm, proxy_authority, user, password) + self.assertEqual(realm, password_manager.realm) + self.assertEqual(proxy_authority, password_manager.url) + + opener.open(request_url) + + self.assertEqual(password_manager.target_realm, realm) + self.assertEqual(password_manager.target_url, proxy_authority) + + self.assertEqual(len(http_handler.requests), 2) + self.assertFalse( + http_handler.requests[0].has_header("Proxy-authorization")) + self.assertTrue( + http_handler.requests[1].has_header("Proxy-authorization")) + self.assertTrue( + http_handler.requests[1] + .get_header("Proxy-authorization").startswith("Digest ")) + + def test_digest_auth_uses_full_url(self): + # The non-proxy case must keep using the full request URL (not + # just the host) for the password lookup, so that sub-URI realm + # matching (HTTPPasswordMgr.is_suburi) keeps working + opener = OpenerDirector() + password_manager = MockPasswordManager() + digest_handler = urllib.request.HTTPDigestAuthHandler(password_manager) + realm = "ACME Networks" + request_url = "http://acme.example.com/protected" + http_handler = MockHTTPHandlerRedirect( + 401, + 'WWW-Authenticate: Digest realm="%s", nonce="abcdef0123456789", ' + 'qop="auth"\r\n\r\n' % realm) + opener.add_handler(digest_handler) + opener.add_handler(http_handler) + + digest_handler.add_password(realm, request_url, "wile", "coyote") + opener.open(request_url) + + self.assertEqual(password_manager.target_realm, realm) + self.assertEqual(password_manager.target_url, request_url) + def test_basic_and_digest_auth_handlers(self): # HTTPDigestAuthHandler raised an exception if it couldn't handle a 40* # response (https://bugs.python.org/issue1479302), where it should instead diff --git a/Lib/test/test_urllib2_localnet.py b/Lib/test/test_urllib2_localnet.py index 9186436183aae1..c2646b9448f297 100644 --- a/Lib/test/test_urllib2_localnet.py +++ b/Lib/test/test_urllib2_localnet.py @@ -351,6 +351,8 @@ def create_fake_proxy_handler(*args, **kwargs): self.server.start() self.server.ready.wait() proxy_url = "http://127.0.0.1:%d" % self.server.port + # Credentials must be registered against proxy authority + self.proxy_authority = "127.0.0.1:%d" % self.server.port handler = urllib.request.ProxyHandler({"http" : proxy_url}) self.proxy_digest_handler = urllib.request.ProxyDigestAuthHandler() self.opener = urllib.request.build_opener( @@ -361,7 +363,7 @@ def stop_server(self): self.server = None def test_proxy_with_bad_password_raises_httperror(self): - self.proxy_digest_handler.add_password(self.REALM, self.URL, + self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority, self.USER, self.PASSWD+"bad") self.digest_auth_handler.set_qop("auth") with self.assertRaises(urllib.error.HTTPError) as cm: @@ -375,7 +377,7 @@ def test_proxy_with_no_password_raises_httperror(self): cm.exception.close() def test_proxy_qop_auth_works(self): - self.proxy_digest_handler.add_password(self.REALM, self.URL, + self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority, self.USER, self.PASSWD) self.digest_auth_handler.set_qop("auth") with self.opener.open(self.URL) as result: @@ -383,7 +385,7 @@ def test_proxy_qop_auth_works(self): pass def test_proxy_qop_auth_int_works_or_throws_urlerror(self): - self.proxy_digest_handler.add_password(self.REALM, self.URL, + self.proxy_digest_handler.add_password(self.REALM, self.proxy_authority, self.USER, self.PASSWD) self.digest_auth_handler.set_qop("auth-int") try: diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 59ed4a7140d59c..c33e0aaac15de2 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1144,7 +1144,13 @@ def get_authorization(self, req, chal): if H is None: return None - user, pw = self.passwd.find_user_password(realm, req.full_url) + # proxy auth is looked up by the proxy's authority + # req.host, set by Request.set_proxy() + if self.auth_header == 'Proxy-Authorization': + authority = req.host + else: + authority = req.full_url + user, pw = self.passwd.find_user_password(realm, authority) if user is None: return None diff --git a/Misc/NEWS.d/next/Library/2026-10-23-21-52-40.gh-issue-60299.Af6IzL.rst b/Misc/NEWS.d/next/Library/2026-10-23-21-52-40.gh-issue-60299.Af6IzL.rst new file mode 100644 index 00000000000000..c23e516e131f9c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-23-21-52-40.gh-issue-60299.Af6IzL.rst @@ -0,0 +1,4 @@ +Fix :class:`urllib.request.ProxyDigestAuthHandler` failing to authenticate. It +looked up credentials using the target request URL instead of the proxy's +authority, so passwords registered via ``add_password()`` against the proxy +never matched. Patch by Senthil Kumaran.