Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jwt/api_jwk.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

101 statements  

1from __future__ import annotations 

2 

3import json 

4import time 

5from typing import Any, Iterator 

6 

7from .algorithms import get_default_algorithms, has_crypto, requires_cryptography 

8from .exceptions import ( 

9 InvalidKeyError, 

10 MissingCryptographyError, 

11 PyJWKError, 

12 PyJWKSetError, 

13 PyJWTError, 

14) 

15from .types import JWKDict 

16 

17 

18class PyJWK: 

19 def __init__(self, jwk_data: JWKDict, algorithm: str | None = None) -> None: 

20 """A class that represents a `JSON Web Key <https://www.rfc-editor.org/rfc/rfc7517>`_. 

21 

22 :param jwk_data: The decoded JWK data. 

23 :type jwk_data: dict[str, typing.Any] 

24 :param algorithm: The key algorithm. If not specified, the key's ``alg`` will be used. 

25 :type algorithm: str or None 

26 :raises InvalidKeyError: If the key type (``kty``) is not found or unsupported, or if the curve (``crv``) is not found or unsupported. 

27 :raises MissingCryptographyError: If the algorithm requires ``cryptography`` to be installed and it is not available. 

28 :raises PyJWKError: If unable to find an algorithm for the key. 

29 """ 

30 self._algorithms = get_default_algorithms() 

31 self._jwk_data = jwk_data 

32 

33 kty = self._jwk_data.get("kty", None) 

34 if not kty: 

35 raise InvalidKeyError(f"kty is not found: {self._jwk_data}") 

36 

37 if not algorithm and isinstance(self._jwk_data, dict): 

38 algorithm = self._jwk_data.get("alg", None) 

39 

40 if not algorithm: 

41 # Determine alg with kty (and crv). 

42 crv = self._jwk_data.get("crv", None) 

43 if kty == "EC": 

44 if crv == "P-256" or not crv: 

45 algorithm = "ES256" 

46 elif crv == "P-384": 

47 algorithm = "ES384" 

48 elif crv == "P-521": 

49 algorithm = "ES512" 

50 elif crv == "secp256k1": 

51 algorithm = "ES256K" 

52 else: 

53 raise InvalidKeyError(f"Unsupported crv: {crv}") 

54 elif kty == "RSA": 

55 algorithm = "RS256" 

56 elif kty == "oct": 

57 algorithm = "HS256" 

58 elif kty == "OKP": 

59 if not crv: 

60 raise InvalidKeyError(f"crv is not found: {self._jwk_data}") 

61 if crv == "Ed25519": 

62 algorithm = "EdDSA" 

63 else: 

64 raise InvalidKeyError(f"Unsupported crv: {crv}") 

65 else: 

66 raise InvalidKeyError(f"Unsupported kty: {kty}") 

67 

68 if not has_crypto and algorithm in requires_cryptography: 

69 raise MissingCryptographyError( 

70 f"{algorithm} requires 'cryptography' to be installed." 

71 ) 

72 

73 self.algorithm_name = algorithm 

74 

75 if algorithm in self._algorithms: 

76 self.Algorithm = self._algorithms[algorithm] 

77 else: 

78 raise PyJWKError(f"Unable to find an algorithm for key: {self._jwk_data}") 

79 

80 self.key = self.Algorithm.from_jwk(self._jwk_data) 

81 

82 @staticmethod 

83 def from_dict(obj: JWKDict, algorithm: str | None = None) -> PyJWK: 

84 """Creates a :class:`PyJWK` object from a JSON-like dictionary. 

85 

86 :param obj: The JWK data, as a dictionary 

87 :type obj: dict[str, typing.Any] 

88 :param algorithm: The key algorithm. If not specified, the key's ``alg`` will be used. 

89 :type algorithm: str or None 

90 :rtype: PyJWK 

91 """ 

92 return PyJWK(obj, algorithm) 

93 

94 @staticmethod 

95 def from_json(data: str, algorithm: None = None) -> PyJWK: 

96 """Create a :class:`PyJWK` object from a JSON string. 

97 Implicitly calls :meth:`PyJWK.from_dict()`. 

98 

99 :param str data: The JWK data, as a JSON string. 

100 :param algorithm: The key algorithm. If not specific, the key's ``alg`` will be used. 

101 :type algorithm: str or None 

102 

103 :rtype: PyJWK 

104 """ 

105 obj = json.loads(data) 

106 return PyJWK.from_dict(obj, algorithm) 

107 

108 @property 

109 def key_type(self) -> str | None: 

110 """The `kty` property from the JWK. 

111 

112 :rtype: str or None 

113 """ 

114 return self._jwk_data.get("kty", None) 

115 

116 @property 

117 def key_id(self) -> str | None: 

118 """The `kid` property from the JWK. 

119 

120 :rtype: str or None 

121 """ 

122 return self._jwk_data.get("kid", None) 

123 

124 @property 

125 def public_key_use(self) -> str | None: 

126 """The `use` property from the JWK. 

127 

128 :rtype: str or None 

129 """ 

130 return self._jwk_data.get("use", None) 

131 

132 

133class PyJWKSet: 

134 def __init__(self, keys: list[JWKDict]) -> None: 

135 self.keys = [] 

136 

137 if not keys: 

138 raise PyJWKSetError("The JWK Set did not contain any keys") 

139 

140 if not isinstance(keys, list): 

141 raise PyJWKSetError("Invalid JWK Set value") 

142 

143 for key in keys: 

144 try: 

145 self.keys.append(PyJWK(key)) 

146 except PyJWTError as error: 

147 if isinstance(error, MissingCryptographyError): 

148 raise error 

149 # skip unusable keys 

150 continue 

151 

152 if len(self.keys) == 0: 

153 raise PyJWKSetError( 

154 "The JWK Set did not contain any usable keys. Perhaps 'cryptography' is not installed?" 

155 ) 

156 

157 @staticmethod 

158 def from_dict(obj: dict[str, Any]) -> PyJWKSet: 

159 keys = obj.get("keys", []) 

160 return PyJWKSet(keys) 

161 

162 @staticmethod 

163 def from_json(data: str) -> PyJWKSet: 

164 obj = json.loads(data) 

165 return PyJWKSet.from_dict(obj) 

166 

167 def __getitem__(self, kid: str) -> PyJWK: 

168 for key in self.keys: 

169 if key.key_id == kid: 

170 return key 

171 raise KeyError(f"keyset has no key for kid: {kid}") 

172 

173 def __iter__(self) -> Iterator[PyJWK]: 

174 return iter(self.keys) 

175 

176 

177class PyJWTSetWithTimestamp: 

178 def __init__(self, jwk_set: PyJWKSet): 

179 self.jwk_set = jwk_set 

180 self.timestamp = time.monotonic() 

181 

182 def get_jwk_set(self) -> PyJWKSet: 

183 return self.jwk_set 

184 

185 def get_timestamp(self) -> float: 

186 return self.timestamp