Coverage for /usr/local/lib/python3.10/site-packages/opal_common-0.0.0-py3.10.egg/opal_common/authentication/signer.py: 63%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 11:54 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 11:54 +0000
1from datetime import datetime, timedelta
2from typing import Optional
3from uuid import UUID
5import jwt
6from jwt.api_jwk import PyJWK
7from opal_common.authentication.types import (
8 JWTAlgorithm,
9 JWTClaims,
10 PrivateKey,
11 PublicKey,
12)
13from opal_common.authentication.verifier import JWTVerifier
14from opal_common.logger import logger
17class InvalidJWTCryptoKeysException(Exception):
18 """Raised when JWT signer provided with invalid crypto keys."""
20 pass
23class JWTSigner(JWTVerifier):
24 """Given cryptographic keys, signs and verifies jwt tokens."""
26 def __init__(
27 self,
28 private_key: Optional[PrivateKey],
29 public_key: Optional[PublicKey],
30 algorithm: JWTAlgorithm,
31 audience: str,
32 issuer: str,
33 ):
34 """Inits the signer if and only if the keys provided to __init__ were
35 generate together are are valid. otherwise will throw.
37 JWT signer can be initialized with empty keys (None),
38 in which case signer.enabled == False.
40 This allows opal to run both in secure mode (which keys, requires jwt authentication)
41 and in insecure mode (good for development and running locally).
43 Args:
44 private_key (PrivateKey): a valid private key or None
45 public_key (PublicKey): a valid public key or None
46 algorithm (JWTAlgorithm): the jwt algorithm to use
47 (possible values: https://pyjwt.readthedocs.io/en/stable/algorithms.html)
48 audience (string): the value for the aud claim: https://tools.ietf.org/html/rfc7519#section-4.1.3
49 issuer (string): the value for the iss claim: https://tools.ietf.org/html/rfc7519#section-4.1.1
50 """
51 super().__init__(
52 public_key=public_key, algorithm=algorithm, audience=audience, issuer=issuer
53 )
54 self._private_key = private_key
55 self._verify_crypto_keys()
57 def _verify_crypto_keys(self):
58 """Verifies whether or not valid crypto keys were provided to the
59 signer. if both keys are valid, encodes and decodes a JWT to make sure
60 the keys match.
62 if both private and public keys are valid and are matching =>
63 signer is enabled if both private and public keys are None =>
64 signer is disabled (self.enabled == False) if only one key is
65 valid/not-None => throws ValueError any other case => throws
66 ValueError
67 """
68 if self._private_key is not None and self._public_key is not None: 68 ↛ 87line 68 didn't jump to line 87 because the condition on line 68 was always true
69 # both keys provided, let's make sure these keys were generated correctly
70 token = jwt.encode(
71 {"some": "payload"}, self._private_key, algorithm=self._algorithm
72 )
73 try:
74 jwt.decode(token, self._public_key, algorithms=[self._algorithm])
75 except jwt.PyJWTError as exc:
76 logger.info(
77 "JWT Signer key verification failed with error: {err}",
78 err=repr(exc),
79 )
80 raise InvalidJWTCryptoKeysException(
81 "private key and public key do not match!"
82 ) from exc
83 # save jwk
84 self._jwk: PyJWK = PyJWK.from_json(
85 self.get_jwk(), algorithm=self._algorithm
86 )
87 elif self._private_key is None and self._public_key is not None:
88 raise ValueError(
89 "JWT Signer not valid, you provided a public key without a private key!"
90 )
91 elif self._private_key is not None and self._public_key is None:
92 raise ValueError(
93 "JWT Signer not valid, you provided a private key without a public key!"
94 )
95 elif self._private_key is None and self._public_key is None:
96 # valid situation, running in dev mode and api security is off
97 self._disable()
98 else:
99 raise ValueError("Invalid JWT Signer input!")
101 def sign(
102 self, sub: UUID, token_lifetime: timedelta, custom_claims: dict = {}
103 ) -> str:
104 payload = {}
105 issued_at = datetime.utcnow()
106 expire_at = issued_at + token_lifetime
107 payload = {
108 "iat": issued_at,
109 "exp": expire_at,
110 "aud": self._audience,
111 "iss": self._issuer,
112 "sub": sub.hex,
113 }
114 if custom_claims: 114 ↛ 117line 114 didn't jump to line 117 because the condition on line 114 was always true
115 payload.update(custom_claims)
117 headers = {}
118 if self._jwk.key_id is not None: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true
119 headers = {"kid": self._jwk.key_id}
120 return jwt.encode(
121 payload, self._private_key, algorithm=self._algorithm, headers=headers
122 )