pypomes-jwt 0.6.9__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pypomes-jwt might be problematic. Click here for more details.
- {pypomes_jwt-0.6.9 → pypomes_jwt-0.7.1}/PKG-INFO +3 -3
- {pypomes_jwt-0.6.9 → pypomes_jwt-0.7.1}/pyproject.toml +4 -3
- pypomes_jwt-0.7.1/src/pypomes_jwt/__init__.py +27 -0
- pypomes_jwt-0.7.1/src/pypomes_jwt/jwt_constants.py +52 -0
- pypomes_jwt-0.7.1/src/pypomes_jwt/jwt_data.py +303 -0
- pypomes_jwt-0.7.1/src/pypomes_jwt/jwt_pomes.py +392 -0
- pypomes_jwt-0.6.9/src/pypomes_jwt/__init__.py +0 -27
- pypomes_jwt-0.6.9/src/pypomes_jwt/jwt_data.py +0 -430
- pypomes_jwt-0.6.9/src/pypomes_jwt/jwt_pomes.py +0 -359
- {pypomes_jwt-0.6.9 → pypomes_jwt-0.7.1}/.gitignore +0 -0
- {pypomes_jwt-0.6.9 → pypomes_jwt-0.7.1}/LICENSE +0 -0
- {pypomes_jwt-0.6.9 → pypomes_jwt-0.7.1}/README.md +0 -0
- {pypomes_jwt-0.6.9 → pypomes_jwt-0.7.1}/src/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: A collection of Python pomes, penyeach (JWT module)
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-JWT
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-JWT/issues
|
|
@@ -10,6 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
|
-
Requires-Dist: cryptography>=44.0.
|
|
13
|
+
Requires-Dist: cryptography>=44.0.2
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
|
-
Requires-Dist: pypomes-core>=1.
|
|
15
|
+
Requires-Dist: pypomes-core>=1.8.3
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pypomes_jwt"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.7.1"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name="GT Nunes", email="wisecoder01@gmail.com" }
|
|
12
12
|
]
|
|
@@ -20,8 +20,9 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
22
|
"PyJWT>=2.10.1",
|
|
23
|
-
"cryptography>=44.0.
|
|
24
|
-
"pypomes_core>=1.
|
|
23
|
+
"cryptography>=44.0.2",
|
|
24
|
+
"pypomes_core>=1.8.3"
|
|
25
|
+
# "pypomes_db>=1.9.1"
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
[project.urls]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .jwt_constants import (
|
|
2
|
+
JWT_DB_ENGINE, JWT_DB_HOST, JWT_DB_NAME,
|
|
3
|
+
JWT_DB_PORT, JWT_DB_USER, JWT_DB_PWD,
|
|
4
|
+
JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
|
|
5
|
+
JWT_ENCODING_KEY, JWT_DECODING_KEY
|
|
6
|
+
)
|
|
7
|
+
from .jwt_pomes import (
|
|
8
|
+
jwt_needed, jwt_verify_request, jwt_claims, jwt_tokens,
|
|
9
|
+
jwt_get_tokens, jwt_get_claims, jwt_validate_token,
|
|
10
|
+
jwt_assert_access, jwt_set_access, jwt_remove_access
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# jwt_constants
|
|
15
|
+
"JWT_DB_ENGINE", "JWT_DB_HOST", "JWT_DB_NAME",
|
|
16
|
+
"JWT_DB_PORT", "JWT_DB_USER", "JWT_DB_PWD",
|
|
17
|
+
"JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
|
|
18
|
+
"JWT_ENCODING_KEY", "JWT_DECODING_KEY",
|
|
19
|
+
# jwt_pomes
|
|
20
|
+
"jwt_needed", "jwt_verify_request", "jwt_claims", "jwt_tokens",
|
|
21
|
+
"jwt_get_tokens", "jwt_get_claims", "jwt_validate_token",
|
|
22
|
+
"jwt_assert_access", "jwt_set_access", "jwt_remove_access"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
from importlib.metadata import version
|
|
26
|
+
__version__ = version("pypomes_jwt")
|
|
27
|
+
__version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives import serialization
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
3
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
|
4
|
+
from pypomes_core import (
|
|
5
|
+
APP_PREFIX,
|
|
6
|
+
env_get_str, env_get_bytes, env_get_int, env_get_bool
|
|
7
|
+
)
|
|
8
|
+
from secrets import token_bytes
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
# database specs for token persistence
|
|
12
|
+
JWT_DB_ENGINE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
|
|
13
|
+
JWT_DB_HOST: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_HOST")
|
|
14
|
+
JWT_DB_NAME: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_NAME")
|
|
15
|
+
JWT_DB_PORT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_DB_PORT")
|
|
16
|
+
JWT_DB_USER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_USER")
|
|
17
|
+
JWT_DB_PWD: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_PWD")
|
|
18
|
+
|
|
19
|
+
JWT_ROTATE_TOKENS: Final[bool] = False \
|
|
20
|
+
if JWT_DB_ENGINE is None else env_get_bool(key=f"{APP_PREFIX}_JWT_ROTATE_TOKENS",
|
|
21
|
+
def_value=False)
|
|
22
|
+
|
|
23
|
+
# one of HS256, HS512, RSA256, RSA512
|
|
24
|
+
JWT_DEFAULT_ALGORITHM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
|
|
25
|
+
def_value="HS256")
|
|
26
|
+
# recommended: between 5 min and 1 hour (set to 5 min)
|
|
27
|
+
JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_AGE",
|
|
28
|
+
def_value=300)
|
|
29
|
+
# recommended: at least 2 hours (set to 24 hours)
|
|
30
|
+
JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
|
|
31
|
+
def_value=86400)
|
|
32
|
+
|
|
33
|
+
# recommended: allow the encode and decode keys to be generated anew when app starts
|
|
34
|
+
__encoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_ENCODE_KEY")
|
|
35
|
+
__decoding_key: bytes
|
|
36
|
+
if JWT_DEFAULT_ALGORITHM in ["HS256", "HS512"]:
|
|
37
|
+
if not __encoding_key:
|
|
38
|
+
__encoding_key = token_bytes(nbytes=32)
|
|
39
|
+
__decoding_key = __encoding_key
|
|
40
|
+
else:
|
|
41
|
+
__decoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_DECODE_KEY")
|
|
42
|
+
if not __encoding_key or not __decoding_key:
|
|
43
|
+
__priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
|
|
44
|
+
key_size=2048)
|
|
45
|
+
__encoding_key = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
|
|
46
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
47
|
+
encryption_algorithm=serialization.NoEncryption())
|
|
48
|
+
__pub_key: RSAPublicKey = __priv_key.public_key()
|
|
49
|
+
__decoding_key = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
|
|
50
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
51
|
+
JWT_ENCODING_KEY: Final[bytes] = __encoding_key
|
|
52
|
+
JWT_DECODING_KEY: Final[bytes] = __decoding_key
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
import requests
|
|
3
|
+
import string
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from logging import Logger
|
|
6
|
+
from pypomes_core import str_random
|
|
7
|
+
from requests import Response
|
|
8
|
+
from threading import Lock
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .jwt_constants import (
|
|
12
|
+
JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JwtData:
|
|
17
|
+
"""
|
|
18
|
+
Shared JWT data for security token access.
|
|
19
|
+
|
|
20
|
+
Instance variables:
|
|
21
|
+
- access_lock: lock for safe multi-threading access
|
|
22
|
+
- access_data: list with dictionaries holding the JWT token data, organized by account ids:
|
|
23
|
+
{
|
|
24
|
+
<account-id>: {
|
|
25
|
+
"reference-url": # the reference URL
|
|
26
|
+
"remote-provider": <bool>, # whether the JWT provider is a remote server
|
|
27
|
+
"request-timeout": <int>, # in seconds - defaults to no timeout
|
|
28
|
+
"access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
|
|
29
|
+
"refresh-max-age": <int>, # in seconds - defaults to JWT_REFRESH_MAX_AGE
|
|
30
|
+
"grace-interval": <int> # time to wait for token to be valid, in seconds
|
|
31
|
+
"token-audience": <string> # the audience the token is intended for
|
|
32
|
+
"token_nonce": <string> # value used to associate a client session with a token
|
|
33
|
+
"claims": {
|
|
34
|
+
"birthdate": <string>, # subject's birth date
|
|
35
|
+
"email": <string>, # subject's email
|
|
36
|
+
"gender": <string>, # subject's gender
|
|
37
|
+
"name": <string>, # subject's name
|
|
38
|
+
"roles": <List[str]>, # subject roles
|
|
39
|
+
"nonce": <string>, # value used to associate a Client session with a token
|
|
40
|
+
...
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
...
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between
|
|
47
|
+
two parties. It is fully described in the RFC 7519, issued by the Internet Engineering Task Force
|
|
48
|
+
(see https://www.rfc-editor.org/rfc/rfc7519.html).
|
|
49
|
+
In this context, claims are pieces of information a token bears, and herein are loosely classified
|
|
50
|
+
as token-related and account-related. All times are UTC.
|
|
51
|
+
|
|
52
|
+
Token-related claims are mostly required claims, and convey information about the token itself:
|
|
53
|
+
"exp": <timestamp> # expiration time
|
|
54
|
+
"iat": <timestamp> # issued at
|
|
55
|
+
"iss": <string> # issuer (for remote providers, URL to obtain and validate the access tokens)
|
|
56
|
+
"jti": <string> # JWT id
|
|
57
|
+
"sub": <string> # subject (the account identification)
|
|
58
|
+
"nat": <string> # nature of token (A: access; R: refresh) - locally issued tokens, only
|
|
59
|
+
# optional:
|
|
60
|
+
"aud": <string> # token audience
|
|
61
|
+
"nbt": <timestamp> # not before time
|
|
62
|
+
|
|
63
|
+
Account-related claims are optional claims, and convey information about the registered account they belong to.
|
|
64
|
+
Alhough they can be freely specified, these are some of the most commonly used claims:
|
|
65
|
+
"birthdate": <string> # subject's birth date
|
|
66
|
+
"email": <string> # subject's email
|
|
67
|
+
"gender": <string> # subject's gender
|
|
68
|
+
"name": <string> # subject's name
|
|
69
|
+
"roles": <List[str]> # subject roles
|
|
70
|
+
"nonce": <string> # value used to associate a client session with a token
|
|
71
|
+
"""
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Initizalize the token access data.
|
|
75
|
+
"""
|
|
76
|
+
self.access_lock: Lock = Lock()
|
|
77
|
+
self.access_data: dict[str, Any] = {}
|
|
78
|
+
|
|
79
|
+
def add_access(self,
|
|
80
|
+
account_id: str,
|
|
81
|
+
reference_url: str,
|
|
82
|
+
claims: dict[str, Any],
|
|
83
|
+
access_max_age: int,
|
|
84
|
+
refresh_max_age: int,
|
|
85
|
+
grace_interval: int,
|
|
86
|
+
token_audience: str,
|
|
87
|
+
token_nonce: str,
|
|
88
|
+
request_timeout: int,
|
|
89
|
+
remote_provider: bool,
|
|
90
|
+
logger: Logger = None) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
|
|
93
|
+
|
|
94
|
+
The parameter *claims* may contain account-related claims, only. Ideally, it should contain,
|
|
95
|
+
at a minimum, "birthdate", "email", "gender", "name", and "roles".
|
|
96
|
+
If the token provider is local, then the token-related claims are created at token issuing time.
|
|
97
|
+
If the token provider is remote, all claims are sent to it at token request time.
|
|
98
|
+
|
|
99
|
+
:param account_id: the account identification
|
|
100
|
+
:param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
|
|
101
|
+
:param claims: the JWT claimset, as key-value pairs
|
|
102
|
+
:param access_max_age: access token duration, in seconds
|
|
103
|
+
:param refresh_max_age: refresh token duration, in seconds
|
|
104
|
+
:param grace_interval: time to wait for token to be valid, in seconds
|
|
105
|
+
:param token_audience: the audience the token is intended for
|
|
106
|
+
:param token_nonce: optional value used to associate a client session with a token
|
|
107
|
+
:param request_timeout: timeout for the requests to the reference URL
|
|
108
|
+
:param remote_provider: whether the JWT provider is a remote server
|
|
109
|
+
:param logger: optional logger
|
|
110
|
+
"""
|
|
111
|
+
# build and store the access data for the account
|
|
112
|
+
with self.access_lock:
|
|
113
|
+
if account_id not in self.access_data:
|
|
114
|
+
self.access_data[account_id] = {
|
|
115
|
+
"reference_url": reference_url,
|
|
116
|
+
"access-max-age": access_max_age,
|
|
117
|
+
"refresh-max-age": refresh_max_age,
|
|
118
|
+
"grace-interval": grace_interval,
|
|
119
|
+
"token-audience": token_audience,
|
|
120
|
+
"token-nonce": token_nonce,
|
|
121
|
+
"request-timeout": request_timeout,
|
|
122
|
+
"remote-provider": remote_provider,
|
|
123
|
+
"claims": claims or {}
|
|
124
|
+
}
|
|
125
|
+
if logger:
|
|
126
|
+
logger.debug(f"JWT data added for '{account_id}'")
|
|
127
|
+
elif logger:
|
|
128
|
+
logger.warning(f"JWT data already exists for '{account_id}'")
|
|
129
|
+
|
|
130
|
+
def remove_access(self,
|
|
131
|
+
account_id: str,
|
|
132
|
+
logger: Logger) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Remove from storage the access data for *account_id*.
|
|
135
|
+
|
|
136
|
+
:param account_id: the account identification
|
|
137
|
+
:param logger: optional logger
|
|
138
|
+
return: *True* if the access data was removed, *False* otherwise
|
|
139
|
+
"""
|
|
140
|
+
account_data: dict[str, Any] | None
|
|
141
|
+
with self.access_lock:
|
|
142
|
+
account_data = self.access_data.pop(account_id, None)
|
|
143
|
+
|
|
144
|
+
if logger:
|
|
145
|
+
if account_data:
|
|
146
|
+
logger.debug(f"Removed JWT data for '{account_id}'")
|
|
147
|
+
else:
|
|
148
|
+
logger.warning(f"No JWT data found for '{account_id}'")
|
|
149
|
+
|
|
150
|
+
return account_data is not None
|
|
151
|
+
|
|
152
|
+
def issue_tokens(self,
|
|
153
|
+
account_id: str,
|
|
154
|
+
account_claims: dict[str, Any] = None,
|
|
155
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Issue and return the JWT access and refresh tokens for *account_id*.
|
|
158
|
+
|
|
159
|
+
Structure of the return data:
|
|
160
|
+
{
|
|
161
|
+
"access_token": <jwt-token>,
|
|
162
|
+
"created_in": <timestamp>,
|
|
163
|
+
"expires_in": <seconds-to-expiration>,
|
|
164
|
+
"refresh_token": <jwt-token>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
:param account_id: the account identification
|
|
168
|
+
:param account_claims: if provided, may supercede registered account-related claims
|
|
169
|
+
:param logger: optional logger
|
|
170
|
+
:return: the JWT token data, or *None* if error
|
|
171
|
+
:raises InvalidTokenError: token is invalid
|
|
172
|
+
:raises InvalidKeyError: authentication key is not in the proper format
|
|
173
|
+
:raises ExpiredSignatureError: token and refresh period have expired
|
|
174
|
+
:raises InvalidSignatureError: signature does not match the one provided as part of the token
|
|
175
|
+
:raises ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
|
|
176
|
+
:raises InvalidAudienceError: 'aud' claim does not match one of the expected audience
|
|
177
|
+
:raises InvalidAlgorithmError: the specified algorithm is not recognized
|
|
178
|
+
:raises InvalidIssuerError: 'iss' claim does not match the expected issuer
|
|
179
|
+
:raises InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
180
|
+
:raises MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
181
|
+
:raises RuntimeError: access data not found for the given *account_id*, or
|
|
182
|
+
the remote JWT provider failed to return a token
|
|
183
|
+
"""
|
|
184
|
+
# initialize the return variable
|
|
185
|
+
result: dict[str, Any] | None = None
|
|
186
|
+
|
|
187
|
+
# process the data in storage
|
|
188
|
+
with (self.access_lock):
|
|
189
|
+
account_data: dict[str, Any] = self.access_data.get(account_id)
|
|
190
|
+
|
|
191
|
+
# was the JWT data obtained ?
|
|
192
|
+
if account_data:
|
|
193
|
+
# yes, proceed
|
|
194
|
+
current_claims: dict[str, Any] = account_data.get("claims").copy()
|
|
195
|
+
if account_claims:
|
|
196
|
+
current_claims.update(current_claims)
|
|
197
|
+
|
|
198
|
+
# obtain new tokens
|
|
199
|
+
current_claims["jti"] = str_random(size=32,
|
|
200
|
+
chars=string.ascii_letters + string.digits)
|
|
201
|
+
current_claims["iss"] = account_data.get("reference-url")
|
|
202
|
+
|
|
203
|
+
# where is the JWT service provider ?
|
|
204
|
+
if account_data.get("remote-provider"):
|
|
205
|
+
# JWT service is being provided by a remote server
|
|
206
|
+
errors: list[str] = []
|
|
207
|
+
# Structure of the return data:
|
|
208
|
+
# {
|
|
209
|
+
# "access_token": <jwt-token>,
|
|
210
|
+
# "created_in": <timestamp>,
|
|
211
|
+
# "expires_in": <seconds-to-expiration>,
|
|
212
|
+
# "refresh_token": <jwt-token>
|
|
213
|
+
# ...
|
|
214
|
+
# }
|
|
215
|
+
result = _jwt_request_token(errors=errors,
|
|
216
|
+
reference_url=current_claims.get("iss"),
|
|
217
|
+
claims=current_claims,
|
|
218
|
+
timeout=account_data.get("request-timeout"),
|
|
219
|
+
logger=logger)
|
|
220
|
+
if errors:
|
|
221
|
+
raise RuntimeError(" - ".join(errors))
|
|
222
|
+
else:
|
|
223
|
+
# JWT service is being provided locally
|
|
224
|
+
just_now: float = datetime.now(tz=timezone.utc).timestamp()
|
|
225
|
+
current_claims["iat"] = just_now
|
|
226
|
+
current_claims["exp"] = just_now + account_data.get("access-max-age")
|
|
227
|
+
current_claims["nat"] = "R"
|
|
228
|
+
# may raise an exception
|
|
229
|
+
refresh_token: str = jwt.encode(payload=current_claims,
|
|
230
|
+
key=JWT_ENCODING_KEY,
|
|
231
|
+
algorithm=JWT_DEFAULT_ALGORITHM)
|
|
232
|
+
current_claims["nat"] = "A"
|
|
233
|
+
# may raise an exception
|
|
234
|
+
access_token: str = jwt.encode(payload=current_claims,
|
|
235
|
+
key=JWT_ENCODING_KEY,
|
|
236
|
+
algorithm=JWT_DEFAULT_ALGORITHM)
|
|
237
|
+
# return the token data
|
|
238
|
+
result = {
|
|
239
|
+
"access_token": access_token,
|
|
240
|
+
"created_in": account_claims.get("iat"),
|
|
241
|
+
"expires_in": account_claims.get("exp"),
|
|
242
|
+
"refresh_token": refresh_token
|
|
243
|
+
}
|
|
244
|
+
else:
|
|
245
|
+
# JWT access data not found
|
|
246
|
+
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
247
|
+
if logger:
|
|
248
|
+
logger.error(err_msg)
|
|
249
|
+
raise RuntimeError(err_msg)
|
|
250
|
+
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _jwt_request_token(errors: list[str],
|
|
255
|
+
reference_url: str,
|
|
256
|
+
claims: dict[str, Any],
|
|
257
|
+
timeout: int = None,
|
|
258
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Obtain and return the JWT token from *reference_url*, along with its duration.
|
|
261
|
+
|
|
262
|
+
Expected structure of the return data:
|
|
263
|
+
{
|
|
264
|
+
"access_token": <jwt-token>,
|
|
265
|
+
"expires_in": <seconds-to-expiration>
|
|
266
|
+
}
|
|
267
|
+
It is up to the invoker to make sure that the *claims* data conform to the requirements
|
|
268
|
+
of the provider issuing the JWT token.
|
|
269
|
+
|
|
270
|
+
:param errors: incidental errors
|
|
271
|
+
:param reference_url: the reference URL for obtaining JWT tokens
|
|
272
|
+
:param claims: the JWT claimset, as expected by the issuing server
|
|
273
|
+
:param timeout: request timeout, in seconds (defaults to *None*)
|
|
274
|
+
:param logger: optional logger
|
|
275
|
+
"""
|
|
276
|
+
# initialize the return variable
|
|
277
|
+
result: dict[str, Any] | None = None
|
|
278
|
+
|
|
279
|
+
# request the JWT token
|
|
280
|
+
if logger:
|
|
281
|
+
logger.debug(f"POST request JWT token to '{reference_url}'")
|
|
282
|
+
response: Response = requests.post(
|
|
283
|
+
url=reference_url,
|
|
284
|
+
json=claims,
|
|
285
|
+
timeout=timeout
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# was the request successful ?
|
|
289
|
+
if response.status_code in [200, 201, 202]:
|
|
290
|
+
# yes, save the access token data returned
|
|
291
|
+
result = response.json()
|
|
292
|
+
if logger:
|
|
293
|
+
logger.debug(f"JWT token obtained: {result}")
|
|
294
|
+
else:
|
|
295
|
+
# no, report the problem
|
|
296
|
+
err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
|
|
297
|
+
if response.text:
|
|
298
|
+
err_msg += f" - {response.text}"
|
|
299
|
+
if logger:
|
|
300
|
+
logger.error(err_msg)
|
|
301
|
+
errors.append(err_msg)
|
|
302
|
+
|
|
303
|
+
return result
|