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._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 self.key = self.Algorithm.from_jwk(self._jwk_data) 

83 

84 @staticmethod 

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

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

87 

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

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

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

91 :type algorithm: str or None 

92 :rtype: PyJWK 

93 """ 

94 return PyJWK(obj, algorithm) 

95 

96 @staticmethod 

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

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

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

100 

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

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

103 :type algorithm: str or None 

104 

105 :rtype: PyJWK 

106 """ 

107 obj = json.loads(data) 

108 return PyJWK.from_dict(obj, algorithm) 

109 

110 @property 

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

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

113 

114 :rtype: str or None 

115 """ 

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

117 

118 @property 

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

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

121 

122 :rtype: str or None 

123 """ 

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

125 

126 @property 

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

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

129 

130 :rtype: str or None 

131 """ 

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

133 

134 

135class PyJWKSet: 

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

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

138 

139 if not keys: 

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

141 

142 if not isinstance(keys, list): 

143 raise PyJWKSetError("Invalid JWK Set value") 

144 

145 for key in keys: 

146 try: 

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

148 except PyJWTError as error: 

149 if isinstance(error, MissingCryptographyError): 

150 raise error 

151 # skip unusable keys 

152 continue 

153 

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

155 raise PyJWKSetError( 

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

157 ) 

158 

159 @staticmethod 

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

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

162 return PyJWKSet(keys) 

163 

164 @staticmethod 

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

166 obj = json.loads(data) 

167 return PyJWKSet.from_dict(obj) 

168 

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

170 for key in self.keys: 

171 if key.key_id == kid: 

172 return key 

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

174 

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

176 return iter(self.keys) 

177 

178 

179class PyJWTSetWithTimestamp: 

180 def __init__(self, jwk_set: PyJWKSet): 

181 self.jwk_set = jwk_set 

182 self.timestamp = time.monotonic() 

183 

184 def get_jwk_set(self) -> PyJWKSet: 

185 return self.jwk_set 

186 

187 def get_timestamp(self) -> float: 

188 return self.timestamp