Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES/12499.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth``
parameters. They will be removed in aiohttp 4.0. Use the new
:func:`~aiohttp.encode_basic_auth` helper together with
``headers={"Authorization": ...}`` (or
``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead.
Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1`
-- by :user:`Dreamsorcerer`.
3 changes: 3 additions & 0 deletions CHANGES/12499.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic
Authentication credentials. Replaces the now-deprecated
:class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`.
3 changes: 2 additions & 1 deletion aiohttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from .connector import AddrInfoType, SocketFactoryType
from .cookiejar import CookieJar, DummyCookieJar
from .formdata import FormData
from .helpers import BasicAuth, ChainMapProxy, ETag
from .helpers import BasicAuth, ChainMapProxy, ETag, encode_basic_auth
from .http import (
HttpVersion,
HttpVersion10,
Expand Down Expand Up @@ -172,6 +172,7 @@
"ChainMapProxy",
"DigestAuthMiddleware",
"ETag",
"encode_basic_auth",
"set_zlib_backend",
# http
"HttpVersion",
Expand Down
49 changes: 49 additions & 0 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,22 @@ def __init__(
if cookies:
self._cookie_jar.update_cookies(cookies)

if auth is not None:
warnings.warn(
"The 'auth' parameter is deprecated and will be removed in v4;"
" pass headers={'Authorization': "
"aiohttp.encode_basic_auth(login, password)} instead",
DeprecationWarning,
stacklevel=2,
)
if proxy_auth is not None:
warnings.warn(
"The 'proxy_auth' parameter is deprecated and will be removed in v4;"
" pass proxy_headers={'Proxy-Authorization': "
"aiohttp.encode_basic_auth(login, password)} instead",
DeprecationWarning,
stacklevel=2,
)
self._connector_owner = connector_owner
self._default_auth = auth
self._version = version
Expand Down Expand Up @@ -516,6 +532,23 @@ async def _request(
if self.closed:
raise RuntimeError("Session is closed")

if auth is not None:
warnings.warn(
"The 'auth' parameter is deprecated and will be removed in v4;"
" pass headers={'Authorization': "
"aiohttp.encode_basic_auth(login, password)} instead",
DeprecationWarning,
stacklevel=3,
)
if proxy_auth is not None:
warnings.warn(
"The 'proxy_auth' parameter is deprecated and will be removed in v4;"
" pass proxy_headers={'Proxy-Authorization': "
"aiohttp.encode_basic_auth(login, password)} instead",
DeprecationWarning,
stacklevel=3,
)

if not isinstance(ssl, SSL_ALLOWED_TYPES):
raise TypeError(
"ssl should be SSLContext, Fingerprint, or bool, "
Expand Down Expand Up @@ -1065,6 +1098,22 @@ async def _ws_connect(
max_msg_size: int = 4 * 1024 * 1024,
decode_text: bool = True,
) -> "ClientWebSocketResponse[bool]":
if auth is not None:
warnings.warn(
"The 'auth' parameter is deprecated and will be removed in v4;"
" pass headers={'Authorization': "
"aiohttp.encode_basic_auth(login, password)} instead",
DeprecationWarning,
stacklevel=3,
)
if proxy_auth is not None:
warnings.warn(
"The 'proxy_auth' parameter is deprecated and will be removed in v4;"
" pass proxy_headers={'Proxy-Authorization': "
"aiohttp.encode_basic_auth(login, password)} instead",
DeprecationWarning,
stacklevel=3,
)
if timeout is not sentinel:
if isinstance(timeout, ClientWSTimeout):
ws_timeout = timeout
Expand Down
3 changes: 2 additions & 1 deletion aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
HeadersDictProxy,
HeadersMixin,
TimerNoop,
_basic_auth_no_warn,
frozen_dataclass_decorator,
is_expected_content_type,
parse_mimetype,
Expand Down Expand Up @@ -807,7 +808,7 @@ def _update_host(self, url: URL) -> None:

# basic auth info
if url.raw_user or url.raw_password:
self.auth = BasicAuth(url.user or "", url.password or "")
self.auth = _basic_auth_no_warn(url.user or "", url.password or "")

def _update_headers(self, headers: CIMultiDict[str]) -> None:
"""Update request headers."""
Expand Down
41 changes: 35 additions & 6 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@
json_re = re.compile(r"^(?:application/|[\w.-]+/[\w.+-]+?\+)json$", re.IGNORECASE)


def encode_basic_auth(login: str, password: str = "", encoding: str = "utf-8") -> str:
"""Encode HTTP Basic Authentication credentials as an Authorization header value.

Returns a string of the form ``"Basic <base64>"`` suitable for use as the
value of the ``Authorization`` (or ``Proxy-Authorization``) header.
"""
if ":" in login:
raise ValueError('A ":" is not allowed in login (RFC 7617#section-2)')
creds = f"{login}:{password}".encode(encoding)
return "Basic " + base64.b64encode(creds).decode(encoding)


class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])):
"""Http basic authentication helper."""

Expand All @@ -168,6 +180,13 @@ def __new__(
if ":" in login:
raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')

warnings.warn(
"BasicAuth is deprecated and will be removed in aiohttp 4.0; "
"use aiohttp.encode_basic_auth() with "
"headers={'Authorization': ...} instead",
DeprecationWarning,
stacklevel=2,
)
return super().__new__(cls, login, password, encoding)

@classmethod
Expand Down Expand Up @@ -197,7 +216,7 @@ def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth":
except ValueError:
raise ValueError("Invalid credentials.")

return cls(username, password, encoding=encoding)
return _basic_auth_no_warn(username, password, encoding)

@classmethod
def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]:
Expand All @@ -208,12 +227,22 @@ def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"
# to already have these values parsed from the netloc in the cache.
if url.raw_user is None and url.raw_password is None:
return None
return cls(url.user or "", url.password or "", encoding=encoding)
return _basic_auth_no_warn(url.user or "", url.password or "", encoding)

def encode(self) -> str:
"""Encode credentials."""
creds = (f"{self.login}:{self.password}").encode(self.encoding)
return "Basic %s" % base64.b64encode(creds).decode(self.encoding)
return encode_basic_auth(self.login, self.password, self.encoding)


def _basic_auth_no_warn(
login: str, password: str = "", encoding: str = "latin1"
) -> BasicAuth:
"""Construct a BasicAuth without emitting the deprecation warning.

For internal use only. Bypasses BasicAuth.__new__ so that aiohttp's own
machinery doesn't trigger deprecation warnings in user code.
"""
return tuple.__new__(BasicAuth, (login, password, encoding))


def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]:
Expand All @@ -222,7 +251,7 @@ def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]:
# to already have these values parsed from the netloc in the cache.
if url.raw_user is None and url.raw_password is None:
return url, None
return url.with_user(None), BasicAuth(url.user or "", url.password or "")
return url.with_user(None), _basic_auth_no_warn(url.user or "", url.password or "")


def netrc_from_env() -> netrc.netrc | None:
Expand Down Expand Up @@ -302,7 +331,7 @@ def basicauth_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> BasicAuth:
if password is None:
password = "" # type: ignore[unreachable]

return BasicAuth(username, password)
return _basic_auth_no_warn(username, password)


def proxies_from_env() -> dict[str, ProxyInfo]:
Expand Down
35 changes: 26 additions & 9 deletions docs/client_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,21 @@ For ``text/plain``::
Authentication
--------------

Instead of setting the ``Authorization`` header directly,
:class:`ClientSession` and individual request methods provide an ``auth``
argument. An instance of :class:`BasicAuth` can be passed in like this::
For HTTP Basic Authentication, build the ``Authorization`` header using
:func:`encode_basic_auth` and pass it via ``headers``::

auth = BasicAuth(login="...", password="...")
async with ClientSession(auth=auth) as session:
from aiohttp import ClientSession, encode_basic_auth

headers = {"Authorization": encode_basic_auth("user", "pass")}
async with ClientSession(headers=headers) as session:
...

.. deprecated:: 3.14

The ``auth`` parameter and the :class:`BasicAuth` class are deprecated and
will be removed in 4.0. Use :func:`encode_basic_auth` together with the
``headers`` parameter as shown above.

For HTTP digest authentication, use the :class:`DigestAuthMiddleware` client middleware::

from aiohttp import ClientSession, DigestAuthMiddleware
Expand Down Expand Up @@ -718,11 +725,13 @@ To connect, use the *proxy* parameter::

It also supports proxy authorization::

from aiohttp import encode_basic_auth

async with aiohttp.ClientSession() as session:
proxy_auth = aiohttp.BasicAuth('user', 'pass')
proxy_headers = {"Proxy-Authorization": encode_basic_auth("user", "pass")}
async with session.get("http://python.org",
proxy="http://proxy.com",
proxy_auth=proxy_auth) as resp:
proxy_headers=proxy_headers) as resp:
print(resp.status)

Authentication credentials can be passed in proxy URL::
Expand All @@ -732,11 +741,19 @@ Authentication credentials can be passed in proxy URL::

And you may set default proxy::

proxy_auth = aiohttp.BasicAuth('user', 'pass')
async with aiohttp.ClientSession(proxy="http://proxy.com", proxy_auth=proxy_auth) as session:
proxy_headers = {"Proxy-Authorization": encode_basic_auth("user", "pass")}
async with aiohttp.ClientSession(
proxy="http://proxy.com", proxy_headers=proxy_headers
) as session:
async with session.get("http://python.org") as resp:
print(resp.status)

.. deprecated:: 3.14

The ``proxy_auth`` parameter is deprecated and will be removed in 4.0. Use
:func:`encode_basic_auth` with ``proxy_headers={"Proxy-Authorization": ...}``
as shown above.

Contrary to the ``requests`` library, it won't read environment
variables by default. But you can do so by passing
``trust_env=True`` into :class:`aiohttp.ClientSession`
Expand Down
26 changes: 25 additions & 1 deletion docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2326,6 +2326,22 @@ Utilities



.. function:: encode_basic_auth(login, password='', encoding='utf-8')

Encode HTTP Basic Authentication credentials as a value suitable for the
``Authorization`` (or ``Proxy-Authorization``) header::

headers = {"Authorization": encode_basic_auth("user", "pass")}

:param str login: login
:param str password: password (``''`` by default)
:param str encoding: encoding (``'utf-8'`` by default)
:return: a string of the form ``"Basic <base64-encoded credentials>"``
:rtype: str

.. versionadded:: 3.14


.. class:: BasicAuth(login, password='', encoding='latin1')
:canonical: aiohttp.helpers.BasicAuth

Expand All @@ -2336,9 +2352,17 @@ Utilities
:param str encoding: encoding (``'latin1'`` by default)


Should be used for specifying authorization data in client API,
Previously this was used for specifying authorization data in client API,
e.g. *auth* parameter for :meth:`ClientSession.request() <aiohttp.ClientSession.request>`.

.. deprecated:: 3.14

Constructing :class:`BasicAuth` is deprecated and will be removed in
4.0. Use :func:`encode_basic_auth` together with the ``headers``
parameter (or ``proxy_headers`` for proxies) instead. The
:meth:`decode` and :meth:`from_url` class methods remain available for
parsing.


.. classmethod:: decode(auth_header, encoding='latin1')

Expand Down
10 changes: 10 additions & 0 deletions tests/test_client_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
from aiohttp.test_utils import TestClient, TestServer
from aiohttp.typedefs import Handler

pytestmark = [
pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"),
pytest.mark.filterwarnings(
r"ignore:The 'auth' parameter is deprecated:DeprecationWarning"
),
pytest.mark.filterwarnings(
r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning"
),
]


@pytest.fixture
def here() -> pathlib.Path:
Expand Down
20 changes: 9 additions & 11 deletions tests/test_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,19 +457,17 @@ async def test_ipv6_nondefault_https_port(make_client_request: _RequestMaker) ->


async def test_basic_auth(make_client_request: _RequestMaker) -> None:
req = make_client_request(
"get", URL("http://python.org"), auth=aiohttp.BasicAuth("nkim", "1234")
)
with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
auth = aiohttp.BasicAuth("nkim", "1234")
req = make_client_request("get", URL("http://python.org"), auth=auth)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"]


async def test_basic_auth_utf8(make_client_request: _RequestMaker) -> None:
req = make_client_request(
"get",
URL("http://python.org"),
auth=aiohttp.BasicAuth("nkim", "секрет", "utf-8"),
)
with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
auth = aiohttp.BasicAuth("nkim", "секрет", "utf-8")
req = make_client_request("get", URL("http://python.org"), auth=auth)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbTrRgdC10LrRgNC10YI=" == req.headers["AUTHORIZATION"]

Expand All @@ -496,9 +494,9 @@ async def test_basic_auth_no_user_from_url(make_client_request: _RequestMaker) -
async def test_basic_auth_from_url_overridden(
make_client_request: _RequestMaker,
) -> None:
req = make_client_request(
"get", URL("http://garbage@python.org"), auth=aiohttp.BasicAuth("nkim", "1234")
)
with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
auth = aiohttp.BasicAuth("nkim", "1234")
req = make_client_request("get", URL("http://garbage@python.org"), auth=auth)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"]
assert "python.org" == req.url.host
Expand Down
Loading
Loading