1from __future__ import annotations
2
3import base64
4import binascii
5import collections.abc as cabc
6import typing as t
7
8from ..http import dump_header
9from ..http import parse_dict_header
10from ..http import quote_header_value
11from .structures import CallbackDict
12
13
14class Authorization:
15 """A parsed ``Authorization`` header.
16
17 :attr:`.Request.authorization` returns an instance if the header is set.
18
19 An instance can be used with the test :class:`.Client` request methods' ``auth``
20 parameter to send the header in test requests.
21
22 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be
23 set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password``
24 parameters.
25
26 .. versionchanged:: 3.2
27 Get/set/delete ``auth["key"]`` and ``auth.key``, along with
28 ``"key" in auth`` and ``auth.get("key")``, are deprecated and will
29 be removed in Werkzeug 4.0. Use ``parameters`` directly instead.
30
31 .. versionchanged:: 2.3
32 The ``token`` parameter and attribute was added to support auth schemes
33 that use 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 @property
67 def username(self) -> str | None:
68 """The ``username`` key from :attr:`parameters`. A shortcut for
69 ``Basic`` auth.
70 """
71 return self.parameters.get("username")
72
73 @property
74 def password(self) -> str | None:
75 """The ``password`` key from :attr:`parameters`. A shortcut for
76 ``Basic`` auth.
77 """
78 return self.parameters.get("password")
79
80 def __getattr__(self, name: str) -> str | None:
81 import warnings
82
83 warnings.warn(
84 f"'auth.{name}' is deprecated and will be removed in Werkzeug 4.0."
85 f" Use 'auth.parameters[\"{name}\"]' instead.",
86 DeprecationWarning,
87 stacklevel=2,
88 )
89 return self.parameters.get(name)
90
91 def __getitem__(self, name: str) -> str | None:
92 import warnings
93
94 warnings.warn(
95 "'auth[key]' is deprecated and will be removed in Werkzeug 4.0."
96 " Use 'auth.parameters[key]' instead.",
97 DeprecationWarning,
98 stacklevel=2,
99 )
100 return self.parameters.get(name)
101
102 def get(self, key: str, default: str | None = None) -> str | None:
103 import warnings
104
105 warnings.warn(
106 "'auth.get(key)' is deprecated and will be removed in Werkzeug 4.0."
107 " Use 'auth.parameters.get(key)' instead.",
108 DeprecationWarning,
109 stacklevel=2,
110 )
111 return self.parameters.get(key, default)
112
113 def __contains__(self, key: str) -> bool:
114 import warnings
115
116 warnings.warn(
117 "'key in auth' is deprecated and will be removed in Werkzeug 4.0."
118 " Use 'key in auth.parameters' instead.",
119 DeprecationWarning,
120 stacklevel=2,
121 )
122 return key in self.parameters
123
124 def __eq__(self, other: object) -> bool:
125 if not isinstance(other, Authorization):
126 return NotImplemented
127
128 return (
129 other.type == self.type
130 and other.token == self.token
131 and other.parameters == self.parameters
132 )
133
134 @classmethod
135 def from_header(cls, value: str | None) -> t.Self | None:
136 """Parse an ``Authorization`` header value and create an instance of
137 this class, or ``None`` if the value is empty.
138
139 :param value: The header value to parse.
140
141 .. versionadded:: 2.3
142 """
143 if not value:
144 return None
145
146 scheme, _, rest = value.partition(" ")
147 scheme = scheme.lower()
148 rest = rest.strip(" \t")
149
150 if scheme == "basic":
151 try:
152 username, _, password = (
153 base64.b64decode(rest, validate=True).decode().partition(":")
154 )
155 except (binascii.Error, UnicodeError):
156 return None
157
158 return cls(scheme, {"username": username, "password": password})
159
160 if "=" in rest.rstrip("="):
161 # = that is not trailing, this is parameters.
162 return cls(scheme, parse_dict_header(rest), None)
163
164 # No = or only trailing =, this is a token.
165 return cls(scheme, None, rest)
166
167 def to_header(self) -> str:
168 """Convert to an ``Authorization`` header value.
169
170 .. versionadded:: 2.0
171 """
172 if self.type == "basic":
173 value = base64.b64encode(
174 f"{self.username}:{self.password}".encode()
175 ).decode("ascii")
176 return f"Basic {value}"
177
178 if self.token is not None:
179 return f"{self.type.title()} {self.token}"
180
181 return f"{self.type.title()} {dump_header(self.parameters)}"
182
183 def __str__(self) -> str:
184 return self.to_header()
185
186 def __repr__(self) -> str:
187 return f"<{type(self).__name__} {self.to_header()}>"
188
189
190class WWWAuthenticate:
191 """A parsed ``WWW-Authenticate`` header.
192
193 Set :attr:`.Response.www_authenticate` to an instance or list of instances to set
194 the header. Modifying the instance will update the header.
195
196 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be
197 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a
198 token.
199
200 .. versionchanged:: 3.2
201 Considered ``False`` if ``type`` is the empty string.
202
203 .. versionchanged:: 3.2
204 Get/set/delete ``auth["key"]`` and ``auth.key``, along with
205 ``"key" in auth`` and ``auth.get("key")``, are deprecated and will
206 be removed in Werkzeug 4.0. Use ``parameters`` directly instead.
207
208 .. versionchanged:: 2.3
209 The ``token`` parameter and attribute was added to support auth schemes
210 that use a token instead of parameters, such as ``Bearer``.
211
212 .. versionchanged:: 2.3
213 The object is no longer a ``dict``.
214
215 .. versionchanged:: 2.3
216 The ``on_update`` parameter was removed.
217 """
218
219 def __init__(
220 self,
221 auth_type: str = "",
222 values: dict[str, str | None] | None = None,
223 token: str | None = None,
224 ):
225 self._type = auth_type.lower()
226 self._parameters: dict[str, str | None] = CallbackDict(
227 values, lambda _: self._trigger_on_update()
228 )
229 self._token = token
230 self._on_update: cabc.Callable[[WWWAuthenticate], None] | None = None
231
232 def _trigger_on_update(self) -> None:
233 if self._on_update is not None:
234 self._on_update(self)
235
236 @property
237 def type(self) -> str:
238 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``."""
239 return self._type
240
241 @type.setter
242 def type(self, value: str) -> None:
243 self._type = value
244 self._trigger_on_update()
245
246 @property
247 def parameters(self) -> dict[str, str | None]:
248 """A dict of parameters for the header. Only one of this or :attr:`token` should
249 have a value for a given scheme.
250 """
251 return self._parameters
252
253 @parameters.setter
254 def parameters(self, value: dict[str, str]) -> None:
255 self._parameters = CallbackDict(value, lambda _: self._trigger_on_update())
256 self._trigger_on_update()
257
258 @property
259 def token(self) -> str | None:
260 """A dict of parameters for the header. Only one of this or :attr:`token` should
261 have a value for a given scheme.
262 """
263 return self._token
264
265 @token.setter
266 def token(self, value: str | None) -> None:
267 """A token for the header. Only one of this or :attr:`parameters` should have a
268 value for a given scheme.
269
270 .. versionadded:: 2.3
271 """
272 self._token = value
273 self._trigger_on_update()
274
275 def __getitem__(self, key: str) -> str | None:
276 import warnings
277
278 warnings.warn(
279 "'auth[key]' is deprecated and will be removed in Werkzeug 4.0."
280 " Use 'auth.parameters[key]' instead.",
281 DeprecationWarning,
282 stacklevel=2,
283 )
284 return self.parameters.get(key)
285
286 def __setitem__(self, key: str, value: str | None) -> None:
287 import warnings
288
289 warnings.warn(
290 "'auth[key] = value' is deprecated and will be removed in"
291 " Werkzeug 4.0. Use 'auth.parameters[key] = value' instead.",
292 DeprecationWarning,
293 stacklevel=2,
294 )
295 if value is None:
296 if key in self.parameters:
297 del self.parameters[key]
298 else:
299 self.parameters[key] = value
300
301 self._trigger_on_update()
302
303 def __delitem__(self, key: str) -> None:
304 import warnings
305
306 warnings.warn(
307 "'del auth[key]' is deprecated and will be removed in Werkzeug 4.0."
308 " Use 'del auth.parameters[key]' instead.",
309 DeprecationWarning,
310 stacklevel=2,
311 )
312
313 if key in self.parameters:
314 del self.parameters[key]
315 self._trigger_on_update()
316
317 def __getattr__(self, name: str) -> str | None:
318 import warnings
319
320 warnings.warn(
321 f"'auth.{name}' is deprecated and will be removed in Werkzeug 4.0."
322 f" Use 'auth.parameters[\"{name}\"]' instead.",
323 DeprecationWarning,
324 stacklevel=2,
325 )
326 return self[name]
327
328 def __setattr__(self, name: str, value: str | None) -> None:
329 if name in {
330 "type",
331 "_type",
332 "parameters",
333 "_parameters",
334 "token",
335 "_token",
336 "_on_update",
337 }:
338 super().__setattr__(name, value)
339 else:
340 import warnings
341
342 warnings.warn(
343 f"'auth.{name} = value' is deprecated and will be removed in"
344 f" Werkzeug 4.0. Use 'auth.parameters[\"{name}\"] = value' instead.",
345 DeprecationWarning,
346 stacklevel=2,
347 )
348 self[name] = value
349
350 def __delattr__(self, name: str) -> None:
351 import warnings
352
353 warnings.warn(
354 f"'del auth.{name}' is deprecated and will be removed in Werkzeug 4.0."
355 f" Use 'del auth.parameters[\"{name}\"]' instead.",
356 DeprecationWarning,
357 stacklevel=2,
358 )
359 del self[name]
360
361 def __contains__(self, key: str) -> bool:
362 import warnings
363
364 warnings.warn(
365 "'key in auth' is deprecated and will be removed in Werkzeug 4.0."
366 " Use 'key in auth.parameters' instead.",
367 DeprecationWarning,
368 stacklevel=2,
369 )
370 return key in self.parameters
371
372 def __eq__(self, other: object) -> bool:
373 if not isinstance(other, WWWAuthenticate):
374 return NotImplemented
375
376 return (
377 other.type == self.type
378 and other.token == self.token
379 and other.parameters == self.parameters
380 )
381
382 def get(self, key: str, default: str | None = None) -> str | None:
383 import warnings
384
385 warnings.warn(
386 "'auth.get(key)' is deprecated and will be removed in Werkzeug 4.0."
387 " Use 'auth.parameters.get(key)' instead.",
388 DeprecationWarning,
389 stacklevel=2,
390 )
391 return self.parameters.get(key, default)
392
393 @classmethod
394 def from_header(cls, value: str | None) -> t.Self:
395 """Parse a ``WWW-Authenticate`` header value and create an instance of
396 this class.
397
398 :param value: The header value to parse.
399
400 .. versionchanged:: 3.2
401 Return an empty instance instead of ``None`` if the value is empty.
402
403 .. versionadded:: 2.3
404 """
405 if not value:
406 return cls()
407
408 scheme, _, rest = value.partition(" ")
409 scheme = scheme.lower()
410 rest = rest.strip(" \t")
411
412 if "=" in rest.rstrip("="):
413 # = that is not trailing, this is parameters.
414 return cls(scheme, parse_dict_header(rest))
415
416 # No = or only trailing =, this is a token or empty.
417 if rest:
418 return cls(scheme, token=rest)
419
420 return cls(scheme)
421
422 def to_header(self) -> str:
423 """Convert to a ``WWW-Authenticate`` header value.
424
425 .. versionchanged:: 3.2
426 Return the empty string if the instance is empty.
427 """
428 if not self:
429 return ""
430
431 if self.token is not None:
432 return f"{self.type.title()} {self.token}"
433
434 if not self.parameters:
435 return self.type.title()
436
437 if self.type == "digest":
438 items = []
439
440 for key, value in self.parameters.items():
441 if key in {"realm", "domain", "nonce", "opaque", "qop"}:
442 value = quote_header_value(value, allow_token=False)
443 else:
444 value = quote_header_value(value)
445
446 items.append(f"{key}={value}")
447
448 return f"Digest {', '.join(items)}"
449
450 return f"{self.type.title()} {dump_header(self.parameters)}"
451
452 def __bool__(self) -> bool:
453 return self._type != ""
454
455 def __str__(self) -> str:
456 return self.to_header()
457
458 def __repr__(self) -> str:
459 return f"<{type(self).__name__} {self.to_header()}>"