https://github.com/python/cpython/commit/edf93de1e88bd63102597b70a83df3d7d650363b
commit: edf93de1e88bd63102597b70a83df3d7d650363b
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: orsenthil <[email protected]>
date: 2026-10-29T19:03:40Z
summary:

[3.14] gh-60299: Fix ProxyDigestAuthHandler credential lookup to use proxy 
authority (GH-160528) (#160915)

gh-60299: Fix ProxyDigestAuthHandler credential lookup to use proxy authority 
(GH-160528)

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.
(cherry picked from commit 9b3e095a5f2d832d3d1534751299ad38bf59ad88)

Co-authored-by: Senthil Kumaran <[email protected]>
Co-authored-by: Claude Sonnet 5 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-23-21-52-40.gh-issue-60299.Af6IzL.rst
M Lib/test/test_urllib2.py
M Lib/test/test_urllib2_localnet.py
M Lib/urllib/request.py

diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py
index e1bfdc474751f1a..16123842235a157 100644
--- a/Lib/test/test_urllib2.py
+++ b/Lib/test/test_urllib2.py
@@ -1734,6 +1734,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 9186436183aae18..c2646b9448f297f 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 4555d970365cff4..e44691a169275aa 100644
--- a/Lib/urllib/request.py
+++ b/Lib/urllib/request.py
@@ -1142,7 +1142,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 000000000000000..c23e516e131f9cc
--- /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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to