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

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

105 statements  

1from __future__ import annotations 

2 

3import json 

4import time 

5from collections.abc import Iterator 

6from typing import Any 

7 

8from .algorithms import get_default_algorithms, has_crypto, requires_cryptography 

9from .exceptions import ( 

10 InvalidKeyError, 

11 MissingCryptographyError, 

12 PyJWKError, 

13 PyJWKSetError, 

14 PyJWTError, 

15) 

16from .types import JWKDict 

17 

18 

19class PyJWK: 

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

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

22 

23 :param jwk_data: The decoded JWK data. 

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

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

26 :type algorithm: str or None 

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

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

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

30 """ 

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 try: 

76 self.Algorithm = get_default_algorithms()[algorithm] 

77 except KeyError: 

78 raise PyJWKError( 

79 f"Unable to find an algorithm for key: {self._jwk_data}", 

80 ) from None 

81 

82 try: 

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

84 except ValueError as error: 

85 raise InvalidKeyError( 

86 f"Unable to construct key from JWK: {error}" 

87 ) from error 

88 

89 @staticmethod 

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

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

92 

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

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

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

96 :type algorithm: str or None 

97 :rtype: PyJWK 

98 """ 

99 return PyJWK(obj, algorithm) 

100 

101 @staticmethod 

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

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

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

105 

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

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

108 :type algorithm: str or None 

109 

110 :rtype: PyJWK 

111 """ 

112 obj = json.loads(data) 

113 return PyJWK.from_dict(obj, algorithm) 

114 

115 @property 

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

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

118 

119 :rtype: str or None 

120 """ 

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

122 

123 @property 

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

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

126 

127 :rtype: str or None 

128 """ 

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

130 

131 @property 

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

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

134 

135 :rtype: str or None 

136 """ 

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

138 

139 

140class PyJWKSet: 

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

142 self.keys: list[PyJWK] = [] 

143 

144 if not keys: 

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

146 

147 if not isinstance(keys, list): 

148 raise PyJWKSetError("Invalid JWK Set value") 

149 

150 for key in keys: 

151 try: 

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

153 except PyJWTError as error: 

154 if isinstance(error, MissingCryptographyError): 

155 raise error 

156 # skip unusable keys 

157 continue 

158 

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

160 raise PyJWKSetError( 

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

162 ) 

163 

164 @staticmethod 

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

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

167 return PyJWKSet(keys) 

168 

169 @staticmethod 

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

171 obj = json.loads(data) 

172 return PyJWKSet.from_dict(obj) 

173 

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

175 for key in self.keys: 

176 if key.key_id == kid: 

177 return key 

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

179 

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

181 return iter(self.keys) 

182 

183 

184class PyJWTSetWithTimestamp: 

185 def __init__(self, jwk_set: PyJWKSet): 

186 self.jwk_set = jwk_set 

187 self.timestamp = time.monotonic() 

188 

189 def get_jwk_set(self) -> PyJWKSet: 

190 return self.jwk_set 

191 

192 def get_timestamp(self) -> float: 

193 return self.timestamp