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
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 self.key = self.Algorithm.from_jwk(self._jwk_data)
84 @staticmethod
85 def from_dict(obj: JWKDict, algorithm: str | None = None) -> PyJWK:
86 """Creates a :class:`PyJWK` object from a JSON-like dictionary.
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)
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()`.
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
105 :rtype: PyJWK
106 """
107 obj = json.loads(data)
108 return PyJWK.from_dict(obj, algorithm)
110 @property
111 def key_type(self) -> str | None:
112 """The `kty` property from the JWK.
114 :rtype: str or None
115 """
116 return self._jwk_data.get("kty", None)
118 @property
119 def key_id(self) -> str | None:
120 """The `kid` property from the JWK.
122 :rtype: str or None
123 """
124 return self._jwk_data.get("kid", None)
126 @property
127 def public_key_use(self) -> str | None:
128 """The `use` property from the JWK.
130 :rtype: str or None
131 """
132 return self._jwk_data.get("use", None)
135class PyJWKSet:
136 def __init__(self, keys: list[JWKDict]) -> None:
137 self.keys: list[PyJWK] = []
139 if not keys:
140 raise PyJWKSetError("The JWK Set did not contain any keys")
142 if not isinstance(keys, list):
143 raise PyJWKSetError("Invalid JWK Set value")
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
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 )
159 @staticmethod
160 def from_dict(obj: dict[str, Any]) -> PyJWKSet:
161 keys = obj.get("keys", [])
162 return PyJWKSet(keys)
164 @staticmethod
165 def from_json(data: str) -> PyJWKSet:
166 obj = json.loads(data)
167 return PyJWKSet.from_dict(obj)
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}")
175 def __iter__(self) -> Iterator[PyJWK]:
176 return iter(self.keys)
179class PyJWTSetWithTimestamp:
180 def __init__(self, jwk_set: PyJWKSet):
181 self.jwk_set = jwk_set
182 self.timestamp = time.monotonic()
184 def get_jwk_set(self) -> PyJWKSet:
185 return self.jwk_set
187 def get_timestamp(self) -> float:
188 return self.timestamp