123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- import hashlib
- import time
- import warnings
- from datetime import datetime
- from datetime import timezone
- from decimal import Decimal
- from numbers import Real
- from ._json import _CompactJSON
- from .encoding import base64_decode
- from .encoding import base64_encode
- from .encoding import want_bytes
- from .exc import BadData
- from .exc import BadHeader
- from .exc import BadPayload
- from .exc import BadSignature
- from .exc import SignatureExpired
- from .serializer import Serializer
- from .signer import HMACAlgorithm
- from .signer import NoneAlgorithm
- class JSONWebSignatureSerializer(Serializer):
- """This serializer implements JSON Web Signature (JWS) support. Only
- supports the JWS Compact Serialization.
- .. deprecated:: 2.0
- Will be removed in ItsDangerous 2.1. Use a dedicated library
- such as authlib.
- """
- jws_algorithms = {
- "HS256": HMACAlgorithm(hashlib.sha256),
- "HS384": HMACAlgorithm(hashlib.sha384),
- "HS512": HMACAlgorithm(hashlib.sha512),
- "none": NoneAlgorithm(),
- }
- #: The default algorithm to use for signature generation
- default_algorithm = "HS512"
- default_serializer = _CompactJSON
- def __init__(
- self,
- secret_key,
- salt=None,
- serializer=None,
- serializer_kwargs=None,
- signer=None,
- signer_kwargs=None,
- algorithm_name=None,
- ):
- warnings.warn(
- "JWS support is deprecated and will be removed in"
- " ItsDangerous 2.1. Use a dedicated JWS/JWT library such as"
- " authlib.",
- DeprecationWarning,
- stacklevel=2,
- )
- super().__init__(
- secret_key,
- salt=salt,
- serializer=serializer,
- serializer_kwargs=serializer_kwargs,
- signer=signer,
- signer_kwargs=signer_kwargs,
- )
- if algorithm_name is None:
- algorithm_name = self.default_algorithm
- self.algorithm_name = algorithm_name
- self.algorithm = self.make_algorithm(algorithm_name)
- def load_payload(self, payload, serializer=None, return_header=False):
- payload = want_bytes(payload)
- if b"." not in payload:
- raise BadPayload('No "." found in value')
- base64d_header, base64d_payload = payload.split(b".", 1)
- try:
- json_header = base64_decode(base64d_header)
- except Exception as e:
- raise BadHeader(
- "Could not base64 decode the header because of an exception",
- original_error=e,
- )
- try:
- json_payload = base64_decode(base64d_payload)
- except Exception as e:
- raise BadPayload(
- "Could not base64 decode the payload because of an exception",
- original_error=e,
- )
- try:
- header = super().load_payload(json_header, serializer=_CompactJSON)
- except BadData as e:
- raise BadHeader(
- "Could not unserialize header because it was malformed",
- original_error=e,
- )
- if not isinstance(header, dict):
- raise BadHeader("Header payload is not a JSON object", header=header)
- payload = super().load_payload(json_payload, serializer=serializer)
- if return_header:
- return payload, header
- return payload
- def dump_payload(self, header, obj):
- base64d_header = base64_encode(
- self.serializer.dumps(header, **self.serializer_kwargs)
- )
- base64d_payload = base64_encode(
- self.serializer.dumps(obj, **self.serializer_kwargs)
- )
- return base64d_header + b"." + base64d_payload
- def make_algorithm(self, algorithm_name):
- try:
- return self.jws_algorithms[algorithm_name]
- except KeyError:
- raise NotImplementedError("Algorithm not supported")
- def make_signer(self, salt=None, algorithm=None):
- if salt is None:
- salt = self.salt
- key_derivation = "none" if salt is None else None
- if algorithm is None:
- algorithm = self.algorithm
- return self.signer(
- self.secret_keys,
- salt=salt,
- sep=".",
- key_derivation=key_derivation,
- algorithm=algorithm,
- )
- def make_header(self, header_fields):
- header = header_fields.copy() if header_fields else {}
- header["alg"] = self.algorithm_name
- return header
- def dumps(self, obj, salt=None, header_fields=None):
- """Like :meth:`.Serializer.dumps` but creates a JSON Web
- Signature. It also allows for specifying additional fields to be
- included in the JWS header.
- """
- header = self.make_header(header_fields)
- signer = self.make_signer(salt, self.algorithm)
- return signer.sign(self.dump_payload(header, obj))
- def loads(self, s, salt=None, return_header=False):
- """Reverse of :meth:`dumps`. If requested via ``return_header``
- it will return a tuple of payload and header.
- """
- payload, header = self.load_payload(
- self.make_signer(salt, self.algorithm).unsign(want_bytes(s)),
- return_header=True,
- )
- if header.get("alg") != self.algorithm_name:
- raise BadHeader("Algorithm mismatch", header=header, payload=payload)
- if return_header:
- return payload, header
- return payload
- def loads_unsafe(self, s, salt=None, return_header=False):
- kwargs = {"return_header": return_header}
- return self._loads_unsafe_impl(s, salt, kwargs, kwargs)
- class TimedJSONWebSignatureSerializer(JSONWebSignatureSerializer):
- """Works like the regular :class:`JSONWebSignatureSerializer` but
- also records the time of the signing and can be used to expire
- signatures.
- JWS currently does not specify this behavior but it mentions a
- possible extension like this in the spec. Expiry date is encoded
- into the header similar to what's specified in `draft-ietf-oauth
- -json-web-token <http://self-issued.info/docs/draft-ietf-oauth-json
- -web-token.html#expDef>`_.
- """
- DEFAULT_EXPIRES_IN = 3600
- def __init__(self, secret_key, expires_in=None, **kwargs):
- super().__init__(secret_key, **kwargs)
- if expires_in is None:
- expires_in = self.DEFAULT_EXPIRES_IN
- self.expires_in = expires_in
- def make_header(self, header_fields):
- header = super().make_header(header_fields)
- iat = self.now()
- exp = iat + self.expires_in
- header["iat"] = iat
- header["exp"] = exp
- return header
- def loads(self, s, salt=None, return_header=False):
- payload, header = super().loads(s, salt, return_header=True)
- if "exp" not in header:
- raise BadSignature("Missing expiry date", payload=payload)
- int_date_error = BadHeader("Expiry date is not an IntDate", payload=payload)
- try:
- header["exp"] = int(header["exp"])
- except ValueError:
- raise int_date_error
- if header["exp"] < 0:
- raise int_date_error
- if header["exp"] < self.now():
- raise SignatureExpired(
- "Signature expired",
- payload=payload,
- date_signed=self.get_issue_date(header),
- )
- if return_header:
- return payload, header
- return payload
- def get_issue_date(self, header):
- """If the header contains the ``iat`` field, return the date the
- signature was issued, as a timezone-aware
- :class:`datetime.datetime` in UTC.
- .. versionchanged:: 2.0
- The timestamp is returned as a timezone-aware ``datetime``
- in UTC rather than a naive ``datetime`` assumed to be UTC.
- """
- rv = header.get("iat")
- if isinstance(rv, (Real, Decimal)):
- return datetime.fromtimestamp(int(rv), tz=timezone.utc)
- def now(self):
- return int(time.time())
|