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
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
1from __future__ import annotations
3import json
4import time
5from collections.abc import Iterator
6from typing import Any
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
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>`_.
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
33 kty = self._jwk_data.get("kty", None)
34 if not kty:
35 raise InvalidKeyError(f"kty is not found: {self._jwk_data}")
37 if not algorithm and isinstance(self._jwk_data, dict):
38 algorithm = self._jwk_data.get("alg", None)
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}")
68 if not has_crypto and algorithm in requires_cryptography:
69 raise MissingCryptographyError(
70 f"{algorithm} requires 'cryptography' to be installed."
71 )
73 self.algorithm_name = algorithm
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
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
89 @staticmethod
90 def from_dict(obj: JWKDict, algorithm: str | None = None) -> PyJWK:
91 """Creates a :class:`PyJWK` object from a JSON-like dictionary.
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)
101 @staticmethod
102 def from_json(data: str, algorithm: str | None = None) -> PyJWK:
103 """Create a :class:`PyJWK` object from a JSON string.
104 Implicitly calls :meth:`PyJWK.from_dict()`.
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
110 :rtype: PyJWK
111 """
112 obj = json.loads(data)
113 return PyJWK.from_dict(obj, algorithm)
115 @property
116 def key_type(self) -> str | None:
117 """The `kty` property from the JWK.
119 :rtype: str or None
120 """
121 return self._jwk_data.get("kty", None)
123 @property
124 def key_id(self) -> str | None:
125 """The `kid` property from the JWK.
127 :rtype: str or None
128 """
129 return self._jwk_data.get("kid", None)
131 @property
132 def public_key_use(self) -> str | None:
133 """The `use` property from the JWK.
135 :rtype: str or None
136 """
137 return self._jwk_data.get("use", None)
140class PyJWKSet:
141 def __init__(self, keys: list[JWKDict]) -> None:
142 self.keys: list[PyJWK] = []
144 if not keys:
145 raise PyJWKSetError("The JWK Set did not contain any keys")
147 if not isinstance(keys, list):
148 raise PyJWKSetError("Invalid JWK Set value")
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
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 )
164 @staticmethod
165 def from_dict(obj: dict[str, Any]) -> PyJWKSet:
166 keys = obj.get("keys", [])
167 return PyJWKSet(keys)
169 @staticmethod
170 def from_json(data: str) -> PyJWKSet:
171 obj = json.loads(data)
172 return PyJWKSet.from_dict(obj)
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}")
180 def __iter__(self) -> Iterator[PyJWK]:
181 return iter(self.keys)
184class PyJWTSetWithTimestamp:
185 def __init__(self, jwk_set: PyJWKSet):
186 self.jwk_set = jwk_set
187 self.timestamp = time.monotonic()
189 def get_jwk_set(self) -> PyJWKSet:
190 return self.jwk_set
192 def get_timestamp(self) -> float:
193 return self.timestamp