api_jwt.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import json
  2. import warnings
  3. from calendar import timegm
  4. from collections.abc import Iterable, Mapping
  5. from datetime import datetime, timedelta, timezone
  6. from typing import Any, Dict, List, Optional, Type, Union
  7. from . import api_jws
  8. from .exceptions import (
  9. DecodeError,
  10. ExpiredSignatureError,
  11. ImmatureSignatureError,
  12. InvalidAudienceError,
  13. InvalidIssuedAtError,
  14. InvalidIssuerError,
  15. MissingRequiredClaimError,
  16. )
  17. class PyJWT:
  18. def __init__(self, options=None):
  19. if options is None:
  20. options = {}
  21. self.options = {**self._get_default_options(), **options}
  22. @staticmethod
  23. def _get_default_options() -> Dict[str, Union[bool, List[str]]]:
  24. return {
  25. "verify_signature": True,
  26. "verify_exp": True,
  27. "verify_nbf": True,
  28. "verify_iat": True,
  29. "verify_aud": True,
  30. "verify_iss": True,
  31. "require": [],
  32. }
  33. def encode(
  34. self,
  35. payload: Dict[str, Any],
  36. key: str,
  37. algorithm: Optional[str] = "HS256",
  38. headers: Optional[Dict] = None,
  39. json_encoder: Optional[Type[json.JSONEncoder]] = None,
  40. ) -> str:
  41. # Check that we get a mapping
  42. if not isinstance(payload, Mapping):
  43. raise TypeError(
  44. "Expecting a mapping object, as JWT only supports "
  45. "JSON objects as payloads."
  46. )
  47. # Payload
  48. payload = payload.copy()
  49. for time_claim in ["exp", "iat", "nbf"]:
  50. # Convert datetime to a intDate value in known time-format claims
  51. if isinstance(payload.get(time_claim), datetime):
  52. payload[time_claim] = timegm(payload[time_claim].utctimetuple())
  53. json_payload = json.dumps(
  54. payload, separators=(",", ":"), cls=json_encoder
  55. ).encode("utf-8")
  56. return api_jws.encode(json_payload, key, algorithm, headers, json_encoder)
  57. def decode_complete(
  58. self,
  59. jwt: str,
  60. key: str = "",
  61. algorithms: Optional[List[str]] = None,
  62. options: Optional[Dict] = None,
  63. **kwargs,
  64. ) -> Dict[str, Any]:
  65. options = dict(options or {}) # shallow-copy or initialize an empty dict
  66. options.setdefault("verify_signature", True)
  67. # If the user has set the legacy `verify` argument, and it doesn't match
  68. # what the relevant `options` entry for the argument is, inform the user
  69. # that they're likely making a mistake.
  70. if "verify" in kwargs and kwargs["verify"] != options["verify_signature"]:
  71. warnings.warn(
  72. "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. "
  73. "The equivalent is setting `verify_signature` to False in the `options` dictionary. "
  74. "This invocation has a mismatch between the kwarg and the option entry.",
  75. category=DeprecationWarning,
  76. )
  77. if not options["verify_signature"]:
  78. options.setdefault("verify_exp", False)
  79. options.setdefault("verify_nbf", False)
  80. options.setdefault("verify_iat", False)
  81. options.setdefault("verify_aud", False)
  82. options.setdefault("verify_iss", False)
  83. if options["verify_signature"] and not algorithms:
  84. raise DecodeError(
  85. 'It is required that you pass in a value for the "algorithms" argument when calling decode().'
  86. )
  87. decoded = api_jws.decode_complete(
  88. jwt,
  89. key=key,
  90. algorithms=algorithms,
  91. options=options,
  92. **kwargs,
  93. )
  94. try:
  95. payload = json.loads(decoded["payload"])
  96. except ValueError as e:
  97. raise DecodeError(f"Invalid payload string: {e}")
  98. if not isinstance(payload, dict):
  99. raise DecodeError("Invalid payload string: must be a json object")
  100. merged_options = {**self.options, **options}
  101. self._validate_claims(payload, merged_options, **kwargs)
  102. decoded["payload"] = payload
  103. return decoded
  104. def decode(
  105. self,
  106. jwt: str,
  107. key: str = "",
  108. algorithms: Optional[List[str]] = None,
  109. options: Optional[Dict] = None,
  110. **kwargs,
  111. ) -> Dict[str, Any]:
  112. decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs)
  113. return decoded["payload"]
  114. def _validate_claims(
  115. self, payload, options, audience=None, issuer=None, leeway=0, **kwargs
  116. ):
  117. if isinstance(leeway, timedelta):
  118. leeway = leeway.total_seconds()
  119. if not isinstance(audience, (bytes, str, type(None), Iterable)):
  120. raise TypeError("audience must be a string, iterable, or None")
  121. self._validate_required_claims(payload, options)
  122. now = timegm(datetime.now(tz=timezone.utc).utctimetuple())
  123. if "iat" in payload and options["verify_iat"]:
  124. self._validate_iat(payload, now, leeway)
  125. if "nbf" in payload and options["verify_nbf"]:
  126. self._validate_nbf(payload, now, leeway)
  127. if "exp" in payload and options["verify_exp"]:
  128. self._validate_exp(payload, now, leeway)
  129. if options["verify_iss"]:
  130. self._validate_iss(payload, issuer)
  131. if options["verify_aud"]:
  132. self._validate_aud(payload, audience)
  133. def _validate_required_claims(self, payload, options):
  134. for claim in options["require"]:
  135. if payload.get(claim) is None:
  136. raise MissingRequiredClaimError(claim)
  137. def _validate_iat(self, payload, now, leeway):
  138. try:
  139. int(payload["iat"])
  140. except ValueError:
  141. raise InvalidIssuedAtError("Issued At claim (iat) must be an integer.")
  142. def _validate_nbf(self, payload, now, leeway):
  143. try:
  144. nbf = int(payload["nbf"])
  145. except ValueError:
  146. raise DecodeError("Not Before claim (nbf) must be an integer.")
  147. if nbf > (now + leeway):
  148. raise ImmatureSignatureError("The token is not yet valid (nbf)")
  149. def _validate_exp(self, payload, now, leeway):
  150. try:
  151. exp = int(payload["exp"])
  152. except ValueError:
  153. raise DecodeError("Expiration Time claim (exp) must be an" " integer.")
  154. if exp < (now - leeway):
  155. raise ExpiredSignatureError("Signature has expired")
  156. def _validate_aud(self, payload, audience):
  157. if audience is None:
  158. if "aud" not in payload or not payload["aud"]:
  159. return
  160. # Application did not specify an audience, but
  161. # the token has the 'aud' claim
  162. raise InvalidAudienceError("Invalid audience")
  163. if "aud" not in payload or not payload["aud"]:
  164. # Application specified an audience, but it could not be
  165. # verified since the token does not contain a claim.
  166. raise MissingRequiredClaimError("aud")
  167. audience_claims = payload["aud"]
  168. if isinstance(audience_claims, str):
  169. audience_claims = [audience_claims]
  170. if not isinstance(audience_claims, list):
  171. raise InvalidAudienceError("Invalid claim format in token")
  172. if any(not isinstance(c, str) for c in audience_claims):
  173. raise InvalidAudienceError("Invalid claim format in token")
  174. if isinstance(audience, str):
  175. audience = [audience]
  176. if all(aud not in audience_claims for aud in audience):
  177. raise InvalidAudienceError("Invalid audience")
  178. def _validate_iss(self, payload, issuer):
  179. if issuer is None:
  180. return
  181. if "iss" not in payload:
  182. raise MissingRequiredClaimError("iss")
  183. if payload["iss"] != issuer:
  184. raise InvalidIssuerError("Invalid issuer")
  185. _jwt_global_obj = PyJWT()
  186. encode = _jwt_global_obj.encode
  187. decode_complete = _jwt_global_obj.decode_complete
  188. decode = _jwt_global_obj.decode