From f21bc20ba956714ada453db59e7d6bbef14d2bbe Mon Sep 17 00:00:00 2001 From: Pierre-Louis Peeters Date: Wed, 9 Oct 2024 16:25:04 +0200 Subject: [PATCH] Make redirect URL auth take precedence over input auth in client --- CHANGES/9436.bugfix.rst | 1 + aiohttp/client.py | 4 ++- docs/client_advanced.rst | 7 +++++ tests/test_client_functional.py | 54 +++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 CHANGES/9436.bugfix.rst diff --git a/CHANGES/9436.bugfix.rst b/CHANGES/9436.bugfix.rst new file mode 100644 index 0000000000..7bd7fbcfe2 --- /dev/null +++ b/CHANGES/9436.bugfix.rst @@ -0,0 +1 @@ +Authentication provided by a redirect now takes precedence over provided ``auth`` when making requests with the client -- by :user:`PLPeeters`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 8ff1e1d43e..97f801e2e4 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -566,7 +566,9 @@ async def _request( "credentials encoded in URL" ) - if auth is None: + # Override the auth with the one from the URL only if we + # have no auth, or if we got an auth from a redirect URL + if auth is None or (history and auth_from_url is not None): auth = auth_from_url if auth is None and ( diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index 754f030d23..01ea3e9dc7 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -67,6 +67,13 @@ argument. An instance of :class:`BasicAuth` can be passed in like this:: async with ClientSession(auth=auth) as session: ... +Note that if the request is redirected and the redirect URL contains +credentials, those credentials will supersede any previously set credentials. +In other words, if ``http://user@example.com`` redirects to +``http://other_user@example.com``, the second request will be authenticated +as ``other_user``. Providing both the ``auth`` parameter and authentication in +the *initial* URL will result in a :exc:`ValueError`. + For other authentication flows, the ``Authorization`` header can be set directly:: diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 99b72cc9f9..371fc856a9 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -2980,6 +2980,60 @@ async def close(self) -> None: assert resp.status == 200 +async def test_creds_redirect_url_creds_precedence( + create_server_for_url_and_handler: Callable[[URL, Handler], Awaitable[TestServer]], +) -> None: + url_from = URL("http://example.com") + url_to = URL("http://user@example.com") + + async def srv(request: web.Request) -> web.Response: + assert request.host == url_from.host + + if request.headers.get(hdrs.AUTHORIZATION) == "Basic dXNlcjpwYXNz": + raise web.HTTPMovedPermanently(url_to) + + return web.Response() + + server = await create_server_for_url_and_handler(url_from, srv) + + etc_hosts = { + (url_from.host, 80): server, + } + + class FakeResolver(AbstractResolver): + async def resolve( + self, + host: str, + port: int = 0, + family: socket.AddressFamily = socket.AF_INET, + ) -> List[ResolveResult]: + server = etc_hosts[(host, port)] + assert server.port is not None + + return [ + { + "hostname": host, + "host": server.host, + "port": server.port, + "family": socket.AF_INET, + "proto": 0, + "flags": socket.AI_NUMERICHOST, + } + ] + + async def close(self) -> None: + """Dummy""" + + connector = aiohttp.TCPConnector(resolver=FakeResolver(), ssl=False) + + async with aiohttp.ClientSession(connector=connector) as client: + resp = await client.get(url_from, auth=aiohttp.BasicAuth("user", "pass")) + assert len(resp.history) == 1 + assert str(resp.url) == "http://example.com" + assert resp.request_info.headers.get("authorization") == "Basic dXNlcjo=" + assert resp.status == 200 + + @pytest.fixture def create_server_for_url_and_handler( aiohttp_server: AiohttpServer, tls_certificate_authority: trustme.CA