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

102 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._algorithms = get_default_algorithms() 

32 self._jwk_data = jwk_data 

33 

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

35 if not kty: 

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

37 

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

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

40 

41 if not algorithm: 

42 # Determine alg with kty (and crv). 

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

44 if kty == "EC": 

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

46 algorithm = "ES256" 

47 elif crv == "P-384": 

48 algorithm = "ES384" 

49 elif crv == "P-521": 

50 algorithm = "ES512" 

51 elif crv == "secp256k1": 

52 algorithm = "ES256K" 

53 else: 

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

55 elif kty == "RSA": 

56 algorithm = "RS256" 

57 elif kty == "oct": 

58 algorithm = "HS256" 

59 elif kty == "OKP": 

60 if not crv: 

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

62 if crv == "Ed25519": 

63 algorithm = "EdDSA" 

64 else: 

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

66 else: 

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

68 

69 if not has_crypto and algorithm in requires_cryptography: 

70 raise MissingCryptographyError( 

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

72 ) 

73 

74 self.algorithm_name = algorithm 

75 

76 if algorithm in self._algorithms: 

77 self.Algorithm = self._algorithms[algorithm] 

78 else: 

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

80 

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

82 

83 @staticmethod 

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

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

86 

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

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

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

90 :type algorithm: str or None 

91 :rtype: PyJWK 

92 """ 

93 return PyJWK(obj, algorithm) 

94 

95 @staticmethod 

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

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

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

99 

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

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

102 :type algorithm: str or None 

103 

104 :rtype: PyJWK 

105 """ 

106 obj = json.loads(data) 

107 return PyJWK.from_dict(obj, algorithm) 

108 

109 @property 

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

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

112 

113 :rtype: str or None 

114 """ 

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

116 

117 @property 

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

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

120 

121 :rtype: str or None 

122 """ 

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

124 

125 @property 

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

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

128 

129 :rtype: str or None 

130 """ 

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

132 

133 

134class PyJWKSet: 

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

136 self.keys = [] 

137 

138 if not keys: 

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

140 

141 if not isinstance(keys, list): 

142 raise PyJWKSetError("Invalid JWK Set value") 

143 

144 for key in keys: 

145 try: 

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

147 except PyJWTError as error: 

148 if isinstance(error, MissingCryptographyError): 

149 raise error 

150 # skip unusable keys 

151 continue 

152 

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

154 raise PyJWKSetError( 

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

156 ) 

157 

158 @staticmethod 

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

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

161 return PyJWKSet(keys) 

162 

163 @staticmethod 

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

165 obj = json.loads(data) 

166 return PyJWKSet.from_dict(obj) 

167 

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

169 for key in self.keys: 

170 if key.key_id == kid: 

171 return key 

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

173 

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

175 return iter(self.keys) 

176 

177 

178class PyJWTSetWithTimestamp: 

179 def __init__(self, jwk_set: PyJWKSet): 

180 self.jwk_set = jwk_set 

181 self.timestamp = time.monotonic() 

182 

183 def get_jwk_set(self) -> PyJWKSet: 

184 return self.jwk_set 

185 

186 def get_timestamp(self) -> float: 

187 return self.timestamp