1from __future__ import annotations
2
3import base64
4import binascii
5import typing as t
6
7from ..http import dump_header
8from ..http import parse_dict_header
9from ..http import quote_header_value
10from .structures import CallbackDict
11
12if t.TYPE_CHECKING:
13 import typing_extensions as te
14
15
16class Authorization:
17 """Represents the parts of an ``Authorization`` request header.
18
19 :attr:`.Request.authorization` returns an instance if the header is set.
20
21 An instance can be used with the test :class:`.Client` request methods' ``auth``
22 parameter to send the header in test requests.
23
24 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be
25 set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password``
26 parameters.
27
28 For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the
29 :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``.
30
31 .. versionchanged:: 2.3
32 The ``token`` parameter and attribute was added to support auth schemes that use
33 a token instead of parameters, such as ``Bearer``.
34
35 .. versionchanged:: 2.3
36 The object is no longer a ``dict``.
37
38 .. versionchanged:: 0.5
39 The object is an immutable dict.
40 """
41
42 def __init__(
43 self,
44 auth_type: str,
45 data: dict[str, str | None] | None = None,
46 token: str | None = None,
47 ) -> None:
48 self.type = auth_type
49 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
50
51 if data is None:
52 data = {}
53
54 self.parameters = data
55 """A dict of parameters parsed from the header. Either this or :attr:`token`
56 will have a value for a given scheme.
57 """
58
59 self.token = token
60 """A token parsed from the header. Either this or :attr:`parameters` will have a
61 value for a given scheme.
62
63 .. versionadded:: 2.3
64 """
65
66 def __getattr__(self, name: str) -> str | None:
67 return self.parameters.get(name)
68
69 def __getitem__(self, name: str) -> str | None:
70 return self.parameters.get(name)
71
72 def get(self, key: str, default: str | None = None) -> str | None:
73 return self.parameters.get(key, default)
74
75 def __contains__(self, key: str) -> bool:
76 return key in self.parameters
77
78 def __eq__(self, other: object) -> bool:
79 if not isinstance(other, Authorization):
80 return NotImplemented
81
82 return (
83 other.type == self.type
84 and other.token == self.token
85 and other.parameters == self.parameters
86 )
87
88 @classmethod
89 def from_header(cls, value: str | None) -> te.Self | None:
90 """Parse an ``Authorization`` header value and return an instance, or ``None``
91 if the value is empty.
92
93 :param value: The header value to parse.
94
95 .. versionadded:: 2.3
96 """
97 if not value:
98 return None
99
100 scheme, _, rest = value.partition(" ")
101 scheme = scheme.lower()
102 rest = rest.strip()
103
104 if scheme == "basic":
105 try:
106 username, _, password = base64.b64decode(rest).decode().partition(":")
107 except (binascii.Error, UnicodeError):
108 return None
109
110 return cls(scheme, {"username": username, "password": password})
111
112 if "=" in rest.rstrip("="):
113 # = that is not trailing, this is parameters.
114 return cls(scheme, parse_dict_header(rest), None)
115
116 # No = or only trailing =, this is a token.
117 return cls(scheme, None, rest)
118
119 def to_header(self) -> str:
120 """Produce an ``Authorization`` header value representing this data.
121
122 .. versionadded:: 2.0
123 """
124 if self.type == "basic":
125 value = base64.b64encode(
126 f"{self.username}:{self.password}".encode()
127 ).decode("ascii")
128 return f"Basic {value}"
129
130 if self.token is not None:
131 return f"{self.type.title()} {self.token}"
132
133 return f"{self.type.title()} {dump_header(self.parameters)}"
134
135 def __str__(self) -> str:
136 return self.to_header()
137
138 def __repr__(self) -> str:
139 return f"<{type(self).__name__} {self.to_header()}>"
140
141
142class WWWAuthenticate:
143 """Represents the parts of a ``WWW-Authenticate`` response header.
144
145 Set :attr:`.Response.www_authenticate` to an instance of list of instances to set
146 values for this header in the response. Modifying this instance will modify the
147 header value.
148
149 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be
150 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a
151 token.
152
153 For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters`
154 dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and
155 ``"key" in auth`` are also provided.
156
157 .. versionchanged:: 2.3
158 The ``token`` parameter and attribute was added to support auth schemes that use
159 a token instead of parameters, such as ``Bearer``.
160
161 .. versionchanged:: 2.3
162 The object is no longer a ``dict``.
163
164 .. versionchanged:: 2.3
165 The ``on_update`` parameter was removed.
166 """
167
168 def __init__(
169 self,
170 auth_type: str,
171 values: dict[str, str | None] | None = None,
172 token: str | None = None,
173 ):
174 self._type = auth_type.lower()
175 self._parameters: dict[str, str | None] = CallbackDict(
176 values, lambda _: self._trigger_on_update()
177 )
178 self._token = token
179 self._on_update: t.Callable[[WWWAuthenticate], None] | None = None
180
181 def _trigger_on_update(self) -> None:
182 if self._on_update is not None:
183 self._on_update(self)
184
185 @property
186 def type(self) -> str:
187 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
188 return self._type
189
190 @type.setter
191 def type(self, value: str) -> None:
192 self._type = value
193 self._trigger_on_update()
194
195 @property
196 def parameters(self) -> dict[str, str | None]:
197 """A dict of parameters for the header. Only one of this or :attr:`token` should
198 have a value for a given scheme.
199 """
200 return self._parameters
201
202 @parameters.setter
203 def parameters(self, value: dict[str, str]) -> None:
204 self._parameters = CallbackDict(value, lambda _: self._trigger_on_update())
205 self._trigger_on_update()
206
207 @property
208 def token(self) -> str | None:
209 """A dict of parameters for the header. Only one of this or :attr:`token` should
210 have a value for a given scheme.
211 """
212 return self._token
213
214 @token.setter
215 def token(self, value: str | None) -> None:
216 """A token for the header. Only one of this or :attr:`parameters` should have a
217 value for a given scheme.
218
219 .. versionadded:: 2.3
220 """
221 self._token = value
222 self._trigger_on_update()
223
224 def __getitem__(self, key: str) -> str | None:
225 return self.parameters.get(key)
226
227 def __setitem__(self, key: str, value: str | None) -> None:
228 if value is None:
229 if key in self.parameters:
230 del self.parameters[key]
231 else:
232 self.parameters[key] = value
233
234 self._trigger_on_update()
235
236 def __delitem__(self, key: str) -> None:
237 if key in self.parameters:
238 del self.parameters[key]
239 self._trigger_on_update()
240
241 def __getattr__(self, name: str) -> str | None:
242 return self[name]
243
244 def __setattr__(self, name: str, value: str | None) -> None:
245 if name in {"_type", "_parameters", "_token", "_on_update"}:
246 super().__setattr__(name, value)
247 else:
248 self[name] = value
249
250 def __delattr__(self, name: str) -> None:
251 del self[name]
252
253 def __contains__(self, key: str) -> bool:
254 return key in self.parameters
255
256 def __eq__(self, other: object) -> bool:
257 if not isinstance(other, WWWAuthenticate):
258 return NotImplemented
259
260 return (
261 other.type == self.type
262 and other.token == self.token
263 and other.parameters == self.parameters
264 )
265
266 def get(self, key: str, default: str | None = None) -> str | None:
267 return self.parameters.get(key, default)
268
269 @classmethod
270 def from_header(cls, value: str | None) -> te.Self | None:
271 """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None``
272 if the value is empty.
273
274 :param value: The header value to parse.
275
276 .. versionadded:: 2.3
277 """
278 if not value:
279 return None
280
281 scheme, _, rest = value.partition(" ")
282 scheme = scheme.lower()
283 rest = rest.strip()
284
285 if "=" in rest.rstrip("="):
286 # = that is not trailing, this is parameters.
287 return cls(scheme, parse_dict_header(rest), None)
288
289 # No = or only trailing =, this is a token.
290 return cls(scheme, None, rest)
291
292 def to_header(self) -> str:
293 """Produce a ``WWW-Authenticate`` header value representing this data."""
294 if self.token is not None:
295 return f"{self.type.title()} {self.token}"
296
297 if self.type == "digest":
298 items = []
299
300 for key, value in self.parameters.items():
301 if key in {"realm", "domain", "nonce", "opaque", "qop"}:
302 value = quote_header_value(value, allow_token=False)
303 else:
304 value = quote_header_value(value)
305
306 items.append(f"{key}={value}")
307
308 return f"Digest {', '.join(items)}"
309
310 return f"{self.type.title()} {dump_header(self.parameters)}"
311
312 def __str__(self) -> str:
313 return self.to_header()
314
315 def __repr__(self) -> str:
316 return f"<{type(self).__name__} {self.to_header()}>"