pypomes-jwt 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl
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/__init__.py +6 -6
- pypomes_jwt/jwt_data.py +81 -112
- pypomes_jwt/jwt_pomes.py +85 -76
- {pypomes_jwt-0.6.0.dist-info → pypomes_jwt-0.6.2.dist-info}/METADATA +3 -3
- pypomes_jwt-0.6.2.dist-info/RECORD +7 -0
- pypomes_jwt-0.6.0.dist-info/RECORD +0 -7
- {pypomes_jwt-0.6.0.dist-info → pypomes_jwt-0.6.2.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.6.0.dist-info → pypomes_jwt-0.6.2.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/__init__.py
CHANGED
|
@@ -5,9 +5,9 @@ from .jwt_pomes import (
|
|
|
5
5
|
JWT_ENDPOINT_URL,
|
|
6
6
|
JWT_ACCESS_MAX_AGE, JWT_REFRESH_MAX_AGE,
|
|
7
7
|
JWT_HS_SECRET_KEY, JWT_RSA_PRIVATE_KEY, JWT_RSA_PUBLIC_KEY,
|
|
8
|
-
jwt_needed, jwt_verify_request,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
jwt_needed, jwt_verify_request, jwt_service,
|
|
9
|
+
jwt_get_token_claims, jwt_get_token, jwt_get_token_data,
|
|
10
|
+
jwt_assert_access, jwt_set_access, jwt_remove_access
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
@@ -17,9 +17,9 @@ __all__ = [
|
|
|
17
17
|
"JWT_ENDPOINT_URL",
|
|
18
18
|
"JWT_ACCESS_MAX_AGE", "JWT_REFRESH_MAX_AGE",
|
|
19
19
|
"JWT_HS_SECRET_KEY", "JWT_RSA_PRIVATE_KEY", "JWT_RSA_PUBLIC_KEY",
|
|
20
|
-
"jwt_needed", "jwt_verify_request",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"jwt_needed", "jwt_verify_request", "jwt_service",
|
|
21
|
+
"jwt_get_token_claims", "jwt_get_token", "jwt_get_token_data",
|
|
22
|
+
"jwt_assert_access", "jwt_set_access", "jwt_remove_access"
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
from importlib.metadata import version
|
pypomes_jwt/jwt_data.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import jwt
|
|
2
|
-
import math
|
|
3
2
|
import requests
|
|
4
3
|
from datetime import datetime, timedelta, timezone
|
|
5
4
|
from jwt.exceptions import InvalidTokenError
|
|
6
5
|
from logging import Logger
|
|
6
|
+
from pypomes_core import str_random
|
|
7
7
|
from requests import Response
|
|
8
8
|
from threading import Lock
|
|
9
9
|
from typing import Any, Literal
|
|
@@ -18,26 +18,35 @@ class JwtData:
|
|
|
18
18
|
- access_data: list with dictionaries holding the JWT token data:
|
|
19
19
|
[
|
|
20
20
|
{
|
|
21
|
-
"
|
|
22
|
-
"exp": <timestamp>,
|
|
23
|
-
"
|
|
24
|
-
"iss": <string>,
|
|
25
|
-
"
|
|
26
|
-
"
|
|
21
|
+
"reserved-claims": { # reserved claims
|
|
22
|
+
"exp": <timestamp>, # expiration time
|
|
23
|
+
"iat": <timestamp> # issued at
|
|
24
|
+
"iss": <string>, # issuer (for remote providers, URL to obtain and validate the access tokens)
|
|
25
|
+
"jti": <string>, # JWT id
|
|
26
|
+
"sub": <string> # subject (the account identification)
|
|
27
|
+
# not used:
|
|
28
|
+
# "aud": <string> # audience
|
|
29
|
+
# "nbt": <timestamp> # not before time
|
|
27
30
|
},
|
|
28
|
-
"
|
|
31
|
+
"public-claims": {
|
|
32
|
+
"birthdate": <string>, # subject's birth date
|
|
33
|
+
"email": <string>, # subject's email
|
|
34
|
+
"gender": <string>, # subject's gender
|
|
35
|
+
"name": <string>, # subject's name
|
|
36
|
+
"roles": <List[str]> # subject roles
|
|
37
|
+
},
|
|
38
|
+
"custom-claims": { # custom claims
|
|
29
39
|
"<custom-claim-key-1>": "<custom-claim-value-1>",
|
|
30
40
|
...
|
|
31
41
|
"<custom-claim-key-n>": "<custom-claim-value-n>"
|
|
32
42
|
},
|
|
33
43
|
"control-data": { # control data
|
|
44
|
+
"remote-provider": <bool>, # whether the JWT provider is a remote server
|
|
34
45
|
"access-token": <jwt-token>, # access token
|
|
35
46
|
"algorithm": <string>, # HS256, HS512, RSA256, RSA512
|
|
36
|
-
"request-timeout": <
|
|
47
|
+
"request-timeout": <int>, # in seconds - defaults to no timeout
|
|
37
48
|
"access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
|
|
38
49
|
"refresh-exp": <timestamp>, # expiration time for the refresh operation
|
|
39
|
-
"reference-url": <url>, # URL to obtain and validate the access tokens
|
|
40
|
-
"remote-provider": <bool>, # whether the JWT provider is a remote server
|
|
41
50
|
"secret-key": <bytes>, # HS secret key
|
|
42
51
|
"private-key": <bytes>, # RSA private key
|
|
43
52
|
"public-key": <bytes>, # RSA public key
|
|
@@ -54,6 +63,7 @@ class JwtData:
|
|
|
54
63
|
self.access_data: list[dict[str, dict[str, Any]]] = []
|
|
55
64
|
|
|
56
65
|
def add_access_data(self,
|
|
66
|
+
account_id: str,
|
|
57
67
|
reference_url: str,
|
|
58
68
|
claims: dict[str, Any],
|
|
59
69
|
algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
|
|
@@ -62,33 +72,37 @@ class JwtData:
|
|
|
62
72
|
secret_key: bytes,
|
|
63
73
|
private_key: bytes,
|
|
64
74
|
public_key: bytes,
|
|
65
|
-
request_timeout:
|
|
75
|
+
request_timeout: int,
|
|
66
76
|
remote_provider: bool,
|
|
67
77
|
logger: Logger = None) -> None:
|
|
68
78
|
"""
|
|
69
|
-
Add to storage the parameters needed to
|
|
79
|
+
Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
|
|
80
|
+
|
|
81
|
+
The parameter *claims* may contain public and custom claims. Currently, the public claims supported
|
|
82
|
+
are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else are considered to be custom
|
|
83
|
+
claims, and are sent to the remote JWT provided, if applicable.
|
|
70
84
|
|
|
71
85
|
Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
|
|
72
86
|
(typically, an acess-key/secret-key pair), have been previously validated elsewhere.
|
|
73
87
|
This situation might change in the future.
|
|
74
88
|
|
|
75
|
-
:param
|
|
89
|
+
:param account_id: the account identification
|
|
90
|
+
:param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
|
|
76
91
|
:param claims: the JWT claimset, as key-value pairs
|
|
77
92
|
:param algorithm: the algorithm used to sign the token with
|
|
78
|
-
:param access_max_age: token duration
|
|
79
|
-
:param refresh_max_age: duration for the refresh operation
|
|
93
|
+
:param access_max_age: token duration (in seconds)
|
|
94
|
+
:param refresh_max_age: duration for the refresh operation (in seconds)
|
|
80
95
|
:param secret_key: secret key for HS authentication
|
|
81
96
|
:param private_key: private key for RSA authentication
|
|
82
97
|
:param public_key: public key for RSA authentication
|
|
83
|
-
:param request_timeout: timeout for the requests to the
|
|
98
|
+
:param request_timeout: timeout for the requests to the reference URL
|
|
84
99
|
:param remote_provider: whether the JWT provider is a remote server
|
|
85
100
|
:param logger: optional logger
|
|
86
101
|
"""
|
|
87
102
|
# Do the access data already exist ?
|
|
88
|
-
if not self.
|
|
103
|
+
if not self.retrieve_access_data(account_id=account_id):
|
|
89
104
|
# no, build control data
|
|
90
105
|
control_data: dict[str, Any] = {
|
|
91
|
-
"reference-url": reference_url,
|
|
92
106
|
"algorithm": algorithm,
|
|
93
107
|
"access-max-age": access_max_age,
|
|
94
108
|
"request-timeout": request_timeout,
|
|
@@ -102,55 +116,59 @@ class JwtData:
|
|
|
102
116
|
control_data["public-key"] = public_key
|
|
103
117
|
|
|
104
118
|
# build claims
|
|
119
|
+
reserved_claims: dict[str, Any] = {
|
|
120
|
+
"sub": account_id,
|
|
121
|
+
"iss": reference_url,
|
|
122
|
+
"exp": "<numeric-UTC-datetime>",
|
|
123
|
+
"iat": "<numeric-UTC-datetime>",
|
|
124
|
+
"jti": "<jwt-id>",
|
|
125
|
+
}
|
|
105
126
|
custom_claims: dict[str, Any] = {}
|
|
106
|
-
|
|
127
|
+
public_claims: dict[str, Any] = {}
|
|
107
128
|
for key, value in claims.items():
|
|
108
|
-
if key in ["
|
|
109
|
-
|
|
129
|
+
if key in ["birthdate", "email", "gender", "name", "roles"]:
|
|
130
|
+
public_claims[key] = value
|
|
110
131
|
else:
|
|
111
132
|
custom_claims[key] = value
|
|
112
|
-
standard_claims["exp"] = datetime(year=2000,
|
|
113
|
-
month=1,
|
|
114
|
-
day=1,
|
|
115
|
-
tzinfo=timezone.utc)
|
|
116
133
|
# store access data
|
|
117
134
|
item_data = {
|
|
118
135
|
"control-data": control_data,
|
|
119
|
-
"
|
|
136
|
+
"reserved-claims": reserved_claims,
|
|
137
|
+
"public-claims": public_claims,
|
|
120
138
|
"custom-claims": custom_claims
|
|
121
139
|
}
|
|
122
140
|
with self.access_lock:
|
|
123
141
|
self.access_data.append(item_data)
|
|
124
142
|
if logger:
|
|
125
|
-
logger.debug(f"JWT data added for '{
|
|
143
|
+
logger.debug(f"JWT data added for '{account_id}': {item_data}")
|
|
126
144
|
elif logger:
|
|
127
|
-
logger.warning(f"JWT data already exists for '{
|
|
145
|
+
logger.warning(f"JWT data already exists for '{account_id}'")
|
|
128
146
|
|
|
129
147
|
def remove_access_data(self,
|
|
130
|
-
|
|
148
|
+
account_id: str,
|
|
131
149
|
logger: Logger) -> None:
|
|
132
150
|
"""
|
|
133
|
-
Remove from storage the access data for *
|
|
151
|
+
Remove from storage the access data for *account_id*.
|
|
134
152
|
|
|
135
|
-
:param
|
|
153
|
+
:param account_id: the account identification
|
|
136
154
|
:param logger: optional logger
|
|
137
155
|
"""
|
|
138
156
|
# obtain the access data item in storage
|
|
139
|
-
item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(
|
|
157
|
+
item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(account_id=account_id,
|
|
140
158
|
logger=logger)
|
|
141
159
|
if item_data:
|
|
142
160
|
with self.access_lock:
|
|
143
161
|
self.access_data.remove(item_data)
|
|
144
162
|
if logger:
|
|
145
|
-
logger.debug(f"Removed JWT data for '{
|
|
163
|
+
logger.debug(f"Removed JWT data for '{account_id}'")
|
|
146
164
|
elif logger:
|
|
147
|
-
logger.warning(f"No JWT data found for '{
|
|
165
|
+
logger.warning(f"No JWT data found for '{account_id}'")
|
|
148
166
|
|
|
149
167
|
def get_token_data(self,
|
|
150
|
-
|
|
168
|
+
account_id: str,
|
|
151
169
|
logger: Logger = None) -> dict[str, Any]:
|
|
152
170
|
"""
|
|
153
|
-
Obtain and return the JWT token for *
|
|
171
|
+
Obtain and return the JWT token for *account_id*, along with its duration.
|
|
154
172
|
|
|
155
173
|
Structure of the return data:
|
|
156
174
|
{
|
|
@@ -158,9 +176,9 @@ class JwtData:
|
|
|
158
176
|
"expires_in": <seconds-to-expiration>
|
|
159
177
|
}
|
|
160
178
|
|
|
161
|
-
:param
|
|
179
|
+
:param account_id: the account identification
|
|
162
180
|
:param logger: optional logger
|
|
163
|
-
:return: the JWT token data, or
|
|
181
|
+
:return: the JWT token data, or *None* if error
|
|
164
182
|
:raises InvalidTokenError: token is invalid
|
|
165
183
|
:raises InvalidKeyError: authentication key is not in the proper format
|
|
166
184
|
:raises ExpiredSignatureError: token and refresh period have expired
|
|
@@ -171,69 +189,64 @@ class JwtData:
|
|
|
171
189
|
:raises InvalidIssuerError: 'iss' claim does not match the expected issuer
|
|
172
190
|
:raises InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
173
191
|
:raises MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
174
|
-
:raises RuntimeError: access data not found for the given *
|
|
192
|
+
:raises RuntimeError: access data not found for the given *account_id*, or
|
|
175
193
|
the remote JWT provider failed to return a token
|
|
176
194
|
"""
|
|
177
195
|
# declare the return variable
|
|
178
196
|
result: dict[str, Any]
|
|
179
197
|
|
|
180
198
|
# obtain the item in storage
|
|
181
|
-
item_data: dict[str, Any] = self.retrieve_access_data(
|
|
199
|
+
item_data: dict[str, Any] = self.retrieve_access_data(account_id=account_id,
|
|
182
200
|
logger=logger)
|
|
183
201
|
# was the JWT data obtained ?
|
|
184
202
|
if item_data:
|
|
185
203
|
# yes, proceed
|
|
186
204
|
control_data: dict[str, Any] = item_data.get("control-data")
|
|
205
|
+
reserved_claims: dict[str, Any] = item_data.get("reserved-claims")
|
|
187
206
|
custom_claims: dict[str, Any] = item_data.get("custom-claims")
|
|
188
|
-
|
|
189
|
-
just_now: datetime = datetime.now(tz=timezone.utc)
|
|
190
|
-
|
|
191
|
-
# is the current token still valid ?
|
|
192
|
-
if just_now > standard_claims.get("exp"):
|
|
193
|
-
# no, obtain a new token
|
|
194
|
-
reference_url: str = control_data.get("reference-url")
|
|
195
|
-
claims: dict[str, Any] = standard_claims.copy()
|
|
196
|
-
claims.update(custom_claims)
|
|
207
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
197
208
|
|
|
209
|
+
# obtain a new token, if the current token has expired
|
|
210
|
+
if just_now > reserved_claims.get("exp"):
|
|
198
211
|
# where is the locus of the JWT service provider ?
|
|
199
212
|
if control_data.get("remote-provider"):
|
|
200
213
|
# JWT service is being provided by a remote server
|
|
201
|
-
if reference_url.find("?") > 0:
|
|
202
|
-
reference_url = reference_url[:reference_url.index("?")]
|
|
203
|
-
claims.pop("exp", None)
|
|
204
214
|
errors: list[str] = []
|
|
205
215
|
result = jwt_request_token(errors=errors,
|
|
206
|
-
reference_url=
|
|
207
|
-
claims=
|
|
216
|
+
reference_url=reserved_claims.get("iss"),
|
|
217
|
+
claims=custom_claims,
|
|
208
218
|
timeout=control_data.get("request-timeout"),
|
|
209
219
|
logger=logger)
|
|
210
220
|
if result:
|
|
211
221
|
with self.access_lock:
|
|
212
222
|
control_data["access-token"] = result.get("access_token")
|
|
213
223
|
duration: int = result.get("expires_in")
|
|
214
|
-
|
|
224
|
+
reserved_claims["exp"] = just_now + duration
|
|
215
225
|
else:
|
|
216
226
|
raise RuntimeError(" - ".join(errors))
|
|
217
227
|
else:
|
|
218
228
|
# JWT service is being provided locally
|
|
219
|
-
|
|
229
|
+
reserved_claims["jti"] = str_random(size=16)
|
|
230
|
+
reserved_claims["iat"] = just_now
|
|
231
|
+
reserved_claims["exp"] = just_now + control_data.get("access-max-age")
|
|
232
|
+
claims: dict[str, Any] = item_data.get("public-claims").copy()
|
|
233
|
+
claims.update(m=reserved_claims)
|
|
234
|
+
claims.update(m=custom_claims)
|
|
220
235
|
# may raise an exception
|
|
221
236
|
token: str = jwt.encode(payload=claims,
|
|
222
237
|
key=control_data.get("secret-key") or control_data.get("private-key"),
|
|
223
238
|
algorithm=control_data.get("algorithm"))
|
|
224
239
|
with self.access_lock:
|
|
225
240
|
control_data["access-token"] = token
|
|
226
|
-
standard_claims["exp"] = claims.get("exp")
|
|
227
241
|
|
|
228
242
|
# return the token
|
|
229
|
-
diff: timedelta = standard_claims.get("exp") - just_now - timedelta(seconds=10)
|
|
230
243
|
result = {
|
|
231
244
|
"access_token": control_data.get("access-token"),
|
|
232
|
-
"expires_in":
|
|
245
|
+
"expires_in": reserved_claims.get("exp") - just_now
|
|
233
246
|
}
|
|
234
247
|
else:
|
|
235
248
|
# JWT access data not found
|
|
236
|
-
err_msg: str = f"No JWT access data found for '{
|
|
249
|
+
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
237
250
|
if logger:
|
|
238
251
|
logger.error(err_msg)
|
|
239
252
|
raise RuntimeError(err_msg)
|
|
@@ -275,50 +288,14 @@ class JwtData:
|
|
|
275
288
|
|
|
276
289
|
return result
|
|
277
290
|
|
|
278
|
-
def assert_access_data(self,
|
|
279
|
-
reference_url: str) -> bool:
|
|
280
|
-
# noinspection HttpUrlsUsage
|
|
281
|
-
"""
|
|
282
|
-
Assert whether access data exists for *reference_url*.
|
|
283
|
-
|
|
284
|
-
For the purpose of locating access data, Protocol indication in *reference_url*
|
|
285
|
-
(typically, *http://* or *https://*), is disregarded. This guarantees
|
|
286
|
-
that processing herein will not be affected by in-transit protocol changes.
|
|
287
|
-
|
|
288
|
-
:param reference_url: the reference URL for obtaining JWT tokens
|
|
289
|
-
:return: *True" is access data is in storage, *False* otherwise
|
|
290
|
-
"""
|
|
291
|
-
# initialize the return variable
|
|
292
|
-
result: bool = False
|
|
293
|
-
|
|
294
|
-
# disregard protocol
|
|
295
|
-
if reference_url.find("://") > 0:
|
|
296
|
-
reference_url = reference_url[reference_url.index("://")+3:]
|
|
297
|
-
|
|
298
|
-
# assert the data
|
|
299
|
-
with self.access_lock:
|
|
300
|
-
for item_data in self.access_data:
|
|
301
|
-
item_url: str = item_data.get("control-data").get("reference-url")
|
|
302
|
-
if item_url.find("://") > 0:
|
|
303
|
-
item_url = item_url[item_url.index("://")+3:]
|
|
304
|
-
if reference_url == item_url:
|
|
305
|
-
result = True
|
|
306
|
-
break
|
|
307
|
-
|
|
308
|
-
return result
|
|
309
|
-
|
|
310
291
|
def retrieve_access_data(self,
|
|
311
|
-
|
|
292
|
+
account_id: str,
|
|
312
293
|
logger: Logger = None) -> dict[str, dict[str, Any]]:
|
|
313
294
|
# noinspection HttpUrlsUsage
|
|
314
295
|
"""
|
|
315
|
-
Retrieve and return the access data in storage for *
|
|
316
|
-
|
|
317
|
-
For the purpose of locating access data, Protocol indication in *reference_url*
|
|
318
|
-
(typically, *http://* or *https://*), is disregarded. This guarantees
|
|
319
|
-
that processing herein will not be affected by in-transit protocol changes.
|
|
296
|
+
Retrieve and return the access data in storage for *account_id*.
|
|
320
297
|
|
|
321
|
-
:param
|
|
298
|
+
:param account_id: the account identification
|
|
322
299
|
:param logger: optional logger
|
|
323
300
|
:return: the corresponding item in storage, or *None* if not found
|
|
324
301
|
"""
|
|
@@ -326,19 +303,11 @@ class JwtData:
|
|
|
326
303
|
result: dict[str, dict[str, Any]] | None = None
|
|
327
304
|
|
|
328
305
|
if logger:
|
|
329
|
-
logger.debug(f"Retrieve access data for
|
|
330
|
-
|
|
331
|
-
# disregard protocol
|
|
332
|
-
if reference_url.find("://") > 0:
|
|
333
|
-
reference_url = reference_url[reference_url.index("://")+3:]
|
|
334
|
-
|
|
306
|
+
logger.debug(f"Retrieve access data for account id '{account_id}'")
|
|
335
307
|
# retrieve the data
|
|
336
308
|
with self.access_lock:
|
|
337
309
|
for item_data in self.access_data:
|
|
338
|
-
|
|
339
|
-
if item_url.find("://") > 0:
|
|
340
|
-
item_url = item_url[item_url.index("://")+3:]
|
|
341
|
-
if reference_url == item_url:
|
|
310
|
+
if account_id == item_data.get("reserved-claims").get("sub"):
|
|
342
311
|
result = item_data
|
|
343
312
|
break
|
|
344
313
|
if logger:
|
|
@@ -350,7 +319,7 @@ class JwtData:
|
|
|
350
319
|
def jwt_request_token(errors: list[str],
|
|
351
320
|
reference_url: str,
|
|
352
321
|
claims: dict[str, Any],
|
|
353
|
-
timeout:
|
|
322
|
+
timeout: int = None,
|
|
354
323
|
logger: Logger = None) -> dict[str, Any]:
|
|
355
324
|
"""
|
|
356
325
|
Obtain and return the JWT token associated with *reference_url*, along with its duration.
|
|
@@ -389,7 +358,7 @@ def jwt_request_token(errors: list[str],
|
|
|
389
358
|
logger.debug(f"JWT token obtained: {result}")
|
|
390
359
|
else:
|
|
391
360
|
# no, report the problem
|
|
392
|
-
err_msg: str = f"POST request
|
|
361
|
+
err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
|
|
393
362
|
if response.text:
|
|
394
363
|
err_msg += f" - {response.text}"
|
|
395
364
|
if logger:
|
pypomes_jwt/jwt_pomes.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
-
from cryptography.hazmat.backends import default_backend
|
|
3
2
|
from cryptography.hazmat.primitives import serialization
|
|
4
3
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
5
4
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
|
@@ -18,8 +17,8 @@ JWT_ACCESS_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCESS_MAX_A
|
|
|
18
17
|
JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX_AGE",
|
|
19
18
|
def_value=43200)
|
|
20
19
|
JWT_HS_SECRET_KEY: Final[bytes] = env_get_bytes(key=f"{APP_PREFIX}_JWT_HS_SECRET_KEY",
|
|
21
|
-
def_value=token_bytes(32))
|
|
22
|
-
# must invoke 'jwt_service()' below
|
|
20
|
+
def_value=token_bytes(nbytes=32))
|
|
21
|
+
# the endpoint must invoke 'jwt_service()' below
|
|
23
22
|
JWT_ENDPOINT_URL: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_ENDPOINT_URL")
|
|
24
23
|
|
|
25
24
|
# obtain a RSA private/public key pair
|
|
@@ -27,10 +26,9 @@ __priv_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PRIVATE_KEY")
|
|
|
27
26
|
__pub_bytes: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_RSA_PUBLIC_KEY")
|
|
28
27
|
if not __priv_bytes or not __pub_bytes:
|
|
29
28
|
__priv_key: RSAPrivateKey = rsa.generate_private_key(public_exponent=65537,
|
|
30
|
-
key_size=
|
|
31
|
-
backend=default_backend())
|
|
29
|
+
key_size=2048)
|
|
32
30
|
__priv_bytes = __priv_key.private_bytes(encoding=serialization.Encoding.PEM,
|
|
33
|
-
format=serialization.PrivateFormat.
|
|
31
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
34
32
|
encryption_algorithm=serialization.NoEncryption())
|
|
35
33
|
__pub_key: RSAPublicKey = __priv_key.public_key()
|
|
36
34
|
__pub_bytes = __pub_key.public_bytes(encoding=serialization.Encoding.PEM,
|
|
@@ -59,21 +57,33 @@ def jwt_needed(func: callable) -> callable:
|
|
|
59
57
|
return wrapper
|
|
60
58
|
|
|
61
59
|
|
|
62
|
-
def
|
|
63
|
-
claims: dict[str, Any],
|
|
64
|
-
algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
|
|
65
|
-
access_max_age: int = JWT_ACCESS_MAX_AGE,
|
|
66
|
-
refresh_max_age: int = JWT_REFRESH_MAX_AGE,
|
|
67
|
-
secret_key: bytes = JWT_HS_SECRET_KEY,
|
|
68
|
-
private_key: bytes = JWT_RSA_PRIVATE_KEY,
|
|
69
|
-
public_key: bytes = JWT_RSA_PUBLIC_KEY,
|
|
70
|
-
request_timeout: int = None,
|
|
71
|
-
remote_provider: bool = True,
|
|
72
|
-
logger: Logger = None) -> None:
|
|
60
|
+
def jwt_assert_access(account_id: str) -> bool:
|
|
73
61
|
"""
|
|
74
|
-
|
|
62
|
+
Determine whether access for *ccount_id* has been established.
|
|
75
63
|
|
|
76
|
-
:param
|
|
64
|
+
:param account_id: the account identification
|
|
65
|
+
:return: *True* if access data exists for *account_id*, *False* otherwise
|
|
66
|
+
"""
|
|
67
|
+
return __jwt_data.retrieve_access_data(account_id=account_id) is not None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def jwt_set_access(account_id: str,
|
|
71
|
+
reference_url: str,
|
|
72
|
+
claims: dict[str, Any],
|
|
73
|
+
algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"] = JWT_DEFAULT_ALGORITHM,
|
|
74
|
+
access_max_age: int = JWT_ACCESS_MAX_AGE,
|
|
75
|
+
refresh_max_age: int = JWT_REFRESH_MAX_AGE,
|
|
76
|
+
secret_key: bytes = JWT_HS_SECRET_KEY,
|
|
77
|
+
private_key: bytes = JWT_RSA_PRIVATE_KEY,
|
|
78
|
+
public_key: bytes = JWT_RSA_PUBLIC_KEY,
|
|
79
|
+
request_timeout: int = None,
|
|
80
|
+
remote_provider: bool = True,
|
|
81
|
+
logger: Logger = None) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Set the data needed to obtain JWT tokens for *account_id*.
|
|
84
|
+
|
|
85
|
+
:param account_id: the account identification
|
|
86
|
+
:param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
|
|
77
87
|
:param claims: the JWT claimset, as key-value pairs
|
|
78
88
|
:param algorithm: the authentication type
|
|
79
89
|
:param access_max_age: token duration, in seconds
|
|
@@ -81,23 +91,24 @@ def jwt_set_service_access(reference_url: str,
|
|
|
81
91
|
:param secret_key: secret key for HS authentication
|
|
82
92
|
:param private_key: private key for RSA authentication
|
|
83
93
|
:param public_key: public key for RSA authentication
|
|
84
|
-
:param request_timeout: timeout for the requests to the
|
|
94
|
+
:param request_timeout: timeout for the requests to the reference URL
|
|
85
95
|
:param remote_provider: whether the JWT provider is a remote server
|
|
86
96
|
:param logger: optional logger
|
|
87
97
|
"""
|
|
88
98
|
if logger:
|
|
89
|
-
logger.debug(msg=f"Register access data for '{
|
|
90
|
-
|
|
99
|
+
logger.debug(msg=f"Register access data for '{account_id}'")
|
|
100
|
+
|
|
101
|
+
# extract the claims provided in the reference URL's query string
|
|
91
102
|
pos: int = reference_url.find("?")
|
|
92
103
|
if pos > 0:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
claims[param.split("=")[0]] = param.split("=")[1]
|
|
104
|
+
params: list[str] = reference_url[pos+1:].split(sep="&")
|
|
105
|
+
for param in params:
|
|
106
|
+
claims[param.split("=")[0]] = param.split("=")[1]
|
|
97
107
|
reference_url = reference_url[:pos]
|
|
98
108
|
|
|
99
109
|
# register the JWT service
|
|
100
|
-
__jwt_data.add_access_data(
|
|
110
|
+
__jwt_data.add_access_data(account_id=account_id,
|
|
111
|
+
reference_url=reference_url,
|
|
101
112
|
claims=claims,
|
|
102
113
|
algorithm=algorithm,
|
|
103
114
|
access_max_age=access_max_age,
|
|
@@ -110,40 +121,40 @@ def jwt_set_service_access(reference_url: str,
|
|
|
110
121
|
logger=logger)
|
|
111
122
|
|
|
112
123
|
|
|
113
|
-
def
|
|
114
|
-
|
|
124
|
+
def jwt_remove_access(account_id: str,
|
|
125
|
+
logger: Logger = None) -> None:
|
|
115
126
|
"""
|
|
116
|
-
Remove from storage the JWT access data for *
|
|
127
|
+
Remove from storage the JWT access data for *account_id*.
|
|
117
128
|
|
|
118
|
-
:param
|
|
129
|
+
:param account_id: the account identification
|
|
119
130
|
:param logger: optional logger
|
|
120
131
|
"""
|
|
121
132
|
if logger:
|
|
122
|
-
logger.debug(msg=f"Remove access data for '{
|
|
133
|
+
logger.debug(msg=f"Remove access data for '{account_id}'")
|
|
123
134
|
|
|
124
|
-
__jwt_data.remove_access_data(
|
|
135
|
+
__jwt_data.remove_access_data(account_id=account_id,
|
|
125
136
|
logger=logger)
|
|
126
137
|
|
|
127
138
|
|
|
128
139
|
def jwt_get_token(errors: list[str],
|
|
129
|
-
|
|
140
|
+
account_id: str,
|
|
130
141
|
logger: Logger = None) -> str:
|
|
131
142
|
"""
|
|
132
|
-
Obtain and return a JWT token
|
|
143
|
+
Obtain and return a JWT token for *account_id*.
|
|
133
144
|
|
|
134
145
|
:param errors: incidental error messages
|
|
135
|
-
:param
|
|
146
|
+
:param account_id: the account identification
|
|
136
147
|
:param logger: optional logger
|
|
137
|
-
:return: the JWT token, or
|
|
148
|
+
:return: the JWT token, or *None* if an error ocurred
|
|
138
149
|
"""
|
|
139
150
|
# inicialize the return variable
|
|
140
151
|
result: str | None = None
|
|
141
152
|
|
|
142
153
|
if logger:
|
|
143
|
-
logger.debug(msg=f"Obtain a JWT token for '{
|
|
154
|
+
logger.debug(msg=f"Obtain a JWT token for '{account_id}'")
|
|
144
155
|
|
|
145
156
|
try:
|
|
146
|
-
token_data: dict[str, Any] = __jwt_data.get_token_data(
|
|
157
|
+
token_data: dict[str, Any] = __jwt_data.get_token_data(account_id=account_id,
|
|
147
158
|
logger=logger)
|
|
148
159
|
result = token_data.get("access_token")
|
|
149
160
|
if logger:
|
|
@@ -157,10 +168,10 @@ def jwt_get_token(errors: list[str],
|
|
|
157
168
|
|
|
158
169
|
|
|
159
170
|
def jwt_get_token_data(errors: list[str],
|
|
160
|
-
|
|
171
|
+
account_id: str,
|
|
161
172
|
logger: Logger = None) -> dict[str, Any]:
|
|
162
173
|
"""
|
|
163
|
-
Obtain and return the JWT token associated with *
|
|
174
|
+
Obtain and return the JWT token associated with *account_id*, along with its duration.
|
|
164
175
|
|
|
165
176
|
Structure of the return data:
|
|
166
177
|
{
|
|
@@ -169,17 +180,17 @@ def jwt_get_token_data(errors: list[str],
|
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
:param errors: incidental error messages
|
|
172
|
-
:param
|
|
183
|
+
:param account_id: the account identification
|
|
173
184
|
:param logger: optional logger
|
|
174
|
-
:return: the JWT token data, or
|
|
185
|
+
:return: the JWT token data, or *None* if error
|
|
175
186
|
"""
|
|
176
187
|
# inicialize the return variable
|
|
177
188
|
result: dict[str, Any] | None = None
|
|
178
189
|
|
|
179
190
|
if logger:
|
|
180
|
-
logger.debug(msg=f"Retrieve JWT token data for '{
|
|
191
|
+
logger.debug(msg=f"Retrieve JWT token data for '{account_id}'")
|
|
181
192
|
try:
|
|
182
|
-
result = __jwt_data.get_token_data(
|
|
193
|
+
result = __jwt_data.get_token_data(account_id=account_id,
|
|
183
194
|
logger=logger)
|
|
184
195
|
if logger:
|
|
185
196
|
logger.debug(msg=f"Data is '{result}'")
|
|
@@ -191,11 +202,11 @@ def jwt_get_token_data(errors: list[str],
|
|
|
191
202
|
return result
|
|
192
203
|
|
|
193
204
|
|
|
194
|
-
def
|
|
195
|
-
|
|
196
|
-
|
|
205
|
+
def jwt_get_token_claims(errors: list[str],
|
|
206
|
+
token: str,
|
|
207
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
197
208
|
"""
|
|
198
|
-
Obtain and return the
|
|
209
|
+
Obtain and return the claims set of a JWT *token*.
|
|
199
210
|
|
|
200
211
|
:param errors: incidental error messages
|
|
201
212
|
:param token: the token to be inspected for claims
|
|
@@ -262,20 +273,22 @@ def jwt_verify_request(request: Request,
|
|
|
262
273
|
return result
|
|
263
274
|
|
|
264
275
|
|
|
265
|
-
def jwt_service(
|
|
276
|
+
def jwt_service(account_id: str = None,
|
|
266
277
|
service_params: dict[str, Any] = None,
|
|
267
278
|
logger: Logger = None) -> Response:
|
|
268
279
|
"""
|
|
269
280
|
Entry point for obtaining JWT tokens.
|
|
270
281
|
|
|
271
|
-
In order to be serviced, the invoker must send, as parameter *service_params* or in the body of the request
|
|
272
|
-
a JSON containing:
|
|
282
|
+
In order to be serviced, the invoker must send, as parameter *service_params* or in the body of the request:
|
|
273
283
|
{
|
|
274
|
-
"
|
|
275
|
-
"<custom-claim-key-1>": "<custom-claim-value-1>", -
|
|
284
|
+
"account-id": "<string>" - required account identification
|
|
285
|
+
"<custom-claim-key-1>": "<custom-claim-value-1>", - optional custom claims
|
|
276
286
|
...
|
|
277
287
|
"<custom-claim-key-n>": "<custom-claim-value-n>"
|
|
278
288
|
}
|
|
289
|
+
If provided, the additional custom claims will be sent to the remote provider, if applicable
|
|
290
|
+
(custom claims currently registered for the account may be overridden).
|
|
291
|
+
|
|
279
292
|
|
|
280
293
|
Structure of the return data:
|
|
281
294
|
{
|
|
@@ -283,7 +296,7 @@ def jwt_service(reference_url: str = None,
|
|
|
283
296
|
"expires_in": <seconds-to-expiration>
|
|
284
297
|
}
|
|
285
298
|
|
|
286
|
-
:param
|
|
299
|
+
:param account_id: the account identification, alternatively passed in JSON
|
|
287
300
|
:param service_params: the optional JSON containing the request parameters (defaults to JSON in body)
|
|
288
301
|
:param logger: optional logger
|
|
289
302
|
:return: the requested JWT token, along with its duration.
|
|
@@ -297,43 +310,39 @@ def jwt_service(reference_url: str = None,
|
|
|
297
310
|
msg += f" from '{request.base_url}'"
|
|
298
311
|
logger.debug(msg=msg)
|
|
299
312
|
|
|
300
|
-
#
|
|
313
|
+
# retrieve the parameters
|
|
301
314
|
# noinspection PyUnusedLocal
|
|
302
315
|
params: dict[str, Any] = service_params or {}
|
|
303
316
|
if not params:
|
|
304
317
|
with contextlib.suppress(Exception):
|
|
305
318
|
params = request.get_json()
|
|
319
|
+
if not account_id:
|
|
320
|
+
account_id = params.get("account-id")
|
|
306
321
|
|
|
307
|
-
#
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
reference_url = params.get("reference-url")
|
|
311
|
-
if reference_url:
|
|
322
|
+
# has the account been identified ?
|
|
323
|
+
if account_id:
|
|
324
|
+
# yes, proceed
|
|
312
325
|
if logger:
|
|
313
|
-
logger.debug(msg=f"
|
|
314
|
-
item_data: dict[str, dict[str, Any]] = __jwt_data.retrieve_access_data(
|
|
315
|
-
logger=logger)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
custom_claims
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
valid = False
|
|
322
|
-
break
|
|
323
|
-
|
|
324
|
-
# obtain the token data
|
|
325
|
-
if valid:
|
|
326
|
+
logger.debug(msg=f"Account identification is '{account_id}'")
|
|
327
|
+
item_data: dict[str, dict[str, Any]] = __jwt_data.retrieve_access_data(account_id=account_id,
|
|
328
|
+
logger=logger) or {}
|
|
329
|
+
custom_claims: dict[str, Any] = item_data.get("custom-claims").copy()
|
|
330
|
+
for key, value in params.items():
|
|
331
|
+
custom_claims[key] = value
|
|
332
|
+
|
|
333
|
+
# obtain the token data
|
|
326
334
|
try:
|
|
327
|
-
token_data: dict[str, Any] = __jwt_data.get_token_data(
|
|
335
|
+
token_data: dict[str, Any] = __jwt_data.get_token_data(account_id=account_id,
|
|
328
336
|
logger=logger)
|
|
329
337
|
result = jsonify(token_data)
|
|
330
338
|
except Exception as e:
|
|
331
|
-
# validation failed
|
|
339
|
+
# token validation failed
|
|
332
340
|
if logger:
|
|
333
341
|
logger.error(msg=str(e))
|
|
334
342
|
result = Response(response=str(e),
|
|
335
343
|
status=401)
|
|
336
344
|
else:
|
|
345
|
+
# no, report the problem
|
|
337
346
|
if logger:
|
|
338
347
|
logger.debug(msg=f"Invalid parameters {service_params}")
|
|
339
348
|
result = Response(response="Invalid parameters",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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.1
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
|
-
Requires-Dist: pypomes-core>=1.7.
|
|
15
|
+
Requires-Dist: pypomes-core>=1.7.8
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pypomes_jwt/__init__.py,sha256=m0USOMlGVUfofwukykKf6DAPq7CRn4SiY6CeNOOiqJ8,998
|
|
2
|
+
pypomes_jwt/jwt_data.py,sha256=auVG-sQJjeeQwAzwkaSV149_qHwLIYqoi-Aa0op9eI8,17830
|
|
3
|
+
pypomes_jwt/jwt_pomes.py,sha256=93o0QC7Phsb_29KaLn9mlfE6nUw8HXadqpCZn-Q8gvI,13891
|
|
4
|
+
pypomes_jwt-0.6.2.dist-info/METADATA,sha256=2rj4pjbMCHX_lOmN59UAMGscCesvMvYQgkzvcSCc50s,599
|
|
5
|
+
pypomes_jwt-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
pypomes_jwt-0.6.2.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
7
|
+
pypomes_jwt-0.6.2.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
pypomes_jwt/__init__.py,sha256=1IyBb94cZjkXMibHrH_vh043b06QFh5UQ6HTYSDau28,978
|
|
2
|
-
pypomes_jwt/jwt_data.py,sha256=z-cQjhWDAtYs67CCxaPm-CeJFOF__OyupFIz0jWugiI,19001
|
|
3
|
-
pypomes_jwt/jwt_pomes.py,sha256=kcoQDepMdeeriCe3oCkQfNctaNyDcIHmhY6uV5Ll6B8,13594
|
|
4
|
-
pypomes_jwt-0.6.0.dist-info/METADATA,sha256=jECOxmllsm_0b4SxSQ_CV0SLTqR0PrIxUQXV9nk1Y-0,599
|
|
5
|
-
pypomes_jwt-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
-
pypomes_jwt-0.6.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
7
|
-
pypomes_jwt-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|