pypomes-jwt 0.6.0__py3-none-any.whl → 0.6.1__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 +79 -112
- pypomes_jwt/jwt_pomes.py +85 -76
- {pypomes_jwt-0.6.0.dist-info → pypomes_jwt-0.6.1.dist-info}/METADATA +3 -3
- pypomes_jwt-0.6.1.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.1.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.6.0.dist-info → pypomes_jwt-0.6.1.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,5 +1,4 @@
|
|
|
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
|
|
@@ -18,26 +17,35 @@ class JwtData:
|
|
|
18
17
|
- access_data: list with dictionaries holding the JWT token data:
|
|
19
18
|
[
|
|
20
19
|
{
|
|
21
|
-
"
|
|
22
|
-
"exp": <timestamp>,
|
|
23
|
-
"
|
|
24
|
-
"iss": <string>,
|
|
25
|
-
"
|
|
26
|
-
"
|
|
20
|
+
"reserved-claims": { # reserved claims
|
|
21
|
+
"exp": <timestamp>, # expiration time
|
|
22
|
+
"iat": <timestamp> # issued at
|
|
23
|
+
"iss": <string>, # issuer (for remote providers, URL to obtain and validate the access tokens)
|
|
24
|
+
"jti": <string>, # JWT id
|
|
25
|
+
"sub": <string> # subject (the account identification)
|
|
26
|
+
# not used:
|
|
27
|
+
# "aud": <string> # audience
|
|
28
|
+
# "nbt": <timestamp> # not before time
|
|
27
29
|
},
|
|
28
|
-
"
|
|
30
|
+
"public-claims": {
|
|
31
|
+
"birthdate": <string>, # subject's birth date
|
|
32
|
+
"email": <string>, # subject's email
|
|
33
|
+
"gender": <string>, # subject's gender
|
|
34
|
+
"name": <string>, # subject's name
|
|
35
|
+
"roles": <List[str]> # subject roles
|
|
36
|
+
},
|
|
37
|
+
"custom-claims": { # custom claims
|
|
29
38
|
"<custom-claim-key-1>": "<custom-claim-value-1>",
|
|
30
39
|
...
|
|
31
40
|
"<custom-claim-key-n>": "<custom-claim-value-n>"
|
|
32
41
|
},
|
|
33
42
|
"control-data": { # control data
|
|
43
|
+
"remote-provider": <bool>, # whether the JWT provider is a remote server
|
|
34
44
|
"access-token": <jwt-token>, # access token
|
|
35
45
|
"algorithm": <string>, # HS256, HS512, RSA256, RSA512
|
|
36
|
-
"request-timeout": <
|
|
46
|
+
"request-timeout": <int>, # in seconds - defaults to no timeout
|
|
37
47
|
"access-max-age": <int>, # in seconds - defaults to JWT_ACCESS_MAX_AGE
|
|
38
48
|
"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
49
|
"secret-key": <bytes>, # HS secret key
|
|
42
50
|
"private-key": <bytes>, # RSA private key
|
|
43
51
|
"public-key": <bytes>, # RSA public key
|
|
@@ -54,6 +62,7 @@ class JwtData:
|
|
|
54
62
|
self.access_data: list[dict[str, dict[str, Any]]] = []
|
|
55
63
|
|
|
56
64
|
def add_access_data(self,
|
|
65
|
+
account_id: str,
|
|
57
66
|
reference_url: str,
|
|
58
67
|
claims: dict[str, Any],
|
|
59
68
|
algorithm: Literal["HS256", "HS512", "RSA256", "RSA512"],
|
|
@@ -62,33 +71,37 @@ class JwtData:
|
|
|
62
71
|
secret_key: bytes,
|
|
63
72
|
private_key: bytes,
|
|
64
73
|
public_key: bytes,
|
|
65
|
-
request_timeout:
|
|
74
|
+
request_timeout: int,
|
|
66
75
|
remote_provider: bool,
|
|
67
76
|
logger: Logger = None) -> None:
|
|
68
77
|
"""
|
|
69
|
-
Add to storage the parameters needed to
|
|
78
|
+
Add to storage the parameters needed to produce and validate JWT tokens for *account_id*.
|
|
79
|
+
|
|
80
|
+
The parameter *claims* may contain public and custom claims. Currently, the public claims supported
|
|
81
|
+
are *birthdate*, *email*, *gender*, *name*, and *roles*. Everything else are considered to be custom
|
|
82
|
+
claims, and are sent to the remote JWT provided, if applicable.
|
|
70
83
|
|
|
71
84
|
Presently, the *refresh_max_age* data is not relevant, as the authorization parameters in *claims*
|
|
72
85
|
(typically, an acess-key/secret-key pair), have been previously validated elsewhere.
|
|
73
86
|
This situation might change in the future.
|
|
74
87
|
|
|
75
|
-
:param
|
|
88
|
+
:param account_id: the account identification
|
|
89
|
+
:param reference_url: the reference URL (for remote providers, URL to obtain and validate the JWT tokens)
|
|
76
90
|
:param claims: the JWT claimset, as key-value pairs
|
|
77
91
|
: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
|
|
92
|
+
:param access_max_age: token duration (in seconds)
|
|
93
|
+
:param refresh_max_age: duration for the refresh operation (in seconds)
|
|
80
94
|
:param secret_key: secret key for HS authentication
|
|
81
95
|
:param private_key: private key for RSA authentication
|
|
82
96
|
:param public_key: public key for RSA authentication
|
|
83
|
-
:param request_timeout: timeout for the requests to the
|
|
97
|
+
:param request_timeout: timeout for the requests to the reference URL
|
|
84
98
|
:param remote_provider: whether the JWT provider is a remote server
|
|
85
99
|
:param logger: optional logger
|
|
86
100
|
"""
|
|
87
101
|
# Do the access data already exist ?
|
|
88
|
-
if not self.
|
|
102
|
+
if not self.retrieve_access_data(account_id=account_id):
|
|
89
103
|
# no, build control data
|
|
90
104
|
control_data: dict[str, Any] = {
|
|
91
|
-
"reference-url": reference_url,
|
|
92
105
|
"algorithm": algorithm,
|
|
93
106
|
"access-max-age": access_max_age,
|
|
94
107
|
"request-timeout": request_timeout,
|
|
@@ -102,55 +115,59 @@ class JwtData:
|
|
|
102
115
|
control_data["public-key"] = public_key
|
|
103
116
|
|
|
104
117
|
# build claims
|
|
118
|
+
reserved_claims: dict[str, Any] = {
|
|
119
|
+
"sub": account_id,
|
|
120
|
+
"iss": reference_url,
|
|
121
|
+
"exp": "<numeric-UTC-datetime>",
|
|
122
|
+
"iat": "<numeric-UTC-datetime>",
|
|
123
|
+
"jti": "<jwt-id",
|
|
124
|
+
}
|
|
105
125
|
custom_claims: dict[str, Any] = {}
|
|
106
|
-
|
|
126
|
+
public_claims: dict[str, Any] = {}
|
|
107
127
|
for key, value in claims.items():
|
|
108
|
-
if key in ["
|
|
109
|
-
|
|
128
|
+
if key in ["birthdate", "email", "gender", "name", "roles"]:
|
|
129
|
+
public_claims[key] = value
|
|
110
130
|
else:
|
|
111
131
|
custom_claims[key] = value
|
|
112
|
-
standard_claims["exp"] = datetime(year=2000,
|
|
113
|
-
month=1,
|
|
114
|
-
day=1,
|
|
115
|
-
tzinfo=timezone.utc)
|
|
116
132
|
# store access data
|
|
117
133
|
item_data = {
|
|
118
134
|
"control-data": control_data,
|
|
119
|
-
"
|
|
135
|
+
"reserved-claims": reserved_claims,
|
|
136
|
+
"public-claims": public_claims,
|
|
120
137
|
"custom-claims": custom_claims
|
|
121
138
|
}
|
|
122
139
|
with self.access_lock:
|
|
123
140
|
self.access_data.append(item_data)
|
|
124
141
|
if logger:
|
|
125
|
-
logger.debug(f"JWT data added for '{
|
|
142
|
+
logger.debug(f"JWT data added for '{account_id}': {item_data}")
|
|
126
143
|
elif logger:
|
|
127
|
-
logger.warning(f"JWT data already exists for '{
|
|
144
|
+
logger.warning(f"JWT data already exists for '{account_id}'")
|
|
128
145
|
|
|
129
146
|
def remove_access_data(self,
|
|
130
|
-
|
|
147
|
+
account_id: str,
|
|
131
148
|
logger: Logger) -> None:
|
|
132
149
|
"""
|
|
133
|
-
Remove from storage the access data for *
|
|
150
|
+
Remove from storage the access data for *account_id*.
|
|
134
151
|
|
|
135
|
-
:param
|
|
152
|
+
:param account_id: the account identification
|
|
136
153
|
:param logger: optional logger
|
|
137
154
|
"""
|
|
138
155
|
# obtain the access data item in storage
|
|
139
|
-
item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(
|
|
156
|
+
item_data: dict[str, dict[str, Any]] = self.retrieve_access_data(account_id=account_id,
|
|
140
157
|
logger=logger)
|
|
141
158
|
if item_data:
|
|
142
159
|
with self.access_lock:
|
|
143
160
|
self.access_data.remove(item_data)
|
|
144
161
|
if logger:
|
|
145
|
-
logger.debug(f"Removed JWT data for '{
|
|
162
|
+
logger.debug(f"Removed JWT data for '{account_id}'")
|
|
146
163
|
elif logger:
|
|
147
|
-
logger.warning(f"No JWT data found for '{
|
|
164
|
+
logger.warning(f"No JWT data found for '{account_id}'")
|
|
148
165
|
|
|
149
166
|
def get_token_data(self,
|
|
150
|
-
|
|
167
|
+
account_id: str,
|
|
151
168
|
logger: Logger = None) -> dict[str, Any]:
|
|
152
169
|
"""
|
|
153
|
-
Obtain and return the JWT token for *
|
|
170
|
+
Obtain and return the JWT token for *account_id*, along with its duration.
|
|
154
171
|
|
|
155
172
|
Structure of the return data:
|
|
156
173
|
{
|
|
@@ -158,9 +175,9 @@ class JwtData:
|
|
|
158
175
|
"expires_in": <seconds-to-expiration>
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
:param
|
|
178
|
+
:param account_id: the account identification
|
|
162
179
|
:param logger: optional logger
|
|
163
|
-
:return: the JWT token data, or
|
|
180
|
+
:return: the JWT token data, or *None* if error
|
|
164
181
|
:raises InvalidTokenError: token is invalid
|
|
165
182
|
:raises InvalidKeyError: authentication key is not in the proper format
|
|
166
183
|
:raises ExpiredSignatureError: token and refresh period have expired
|
|
@@ -171,69 +188,63 @@ class JwtData:
|
|
|
171
188
|
:raises InvalidIssuerError: 'iss' claim does not match the expected issuer
|
|
172
189
|
:raises InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
173
190
|
:raises MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
174
|
-
:raises RuntimeError: access data not found for the given *
|
|
191
|
+
:raises RuntimeError: access data not found for the given *account_id*, or
|
|
175
192
|
the remote JWT provider failed to return a token
|
|
176
193
|
"""
|
|
177
194
|
# declare the return variable
|
|
178
195
|
result: dict[str, Any]
|
|
179
196
|
|
|
180
197
|
# obtain the item in storage
|
|
181
|
-
item_data: dict[str, Any] = self.retrieve_access_data(
|
|
198
|
+
item_data: dict[str, Any] = self.retrieve_access_data(account_id=account_id,
|
|
182
199
|
logger=logger)
|
|
183
200
|
# was the JWT data obtained ?
|
|
184
201
|
if item_data:
|
|
185
202
|
# yes, proceed
|
|
186
203
|
control_data: dict[str, Any] = item_data.get("control-data")
|
|
204
|
+
reserved_claims: dict[str, Any] = item_data.get("reserved-claims")
|
|
187
205
|
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)
|
|
206
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
197
207
|
|
|
208
|
+
# obtain a new token, if the current token has expired
|
|
209
|
+
if just_now > reserved_claims.get("exp"):
|
|
198
210
|
# where is the locus of the JWT service provider ?
|
|
199
211
|
if control_data.get("remote-provider"):
|
|
200
212
|
# 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
213
|
errors: list[str] = []
|
|
205
214
|
result = jwt_request_token(errors=errors,
|
|
206
|
-
reference_url=
|
|
207
|
-
claims=
|
|
215
|
+
reference_url=reserved_claims.get("iss"),
|
|
216
|
+
claims=custom_claims,
|
|
208
217
|
timeout=control_data.get("request-timeout"),
|
|
209
218
|
logger=logger)
|
|
210
219
|
if result:
|
|
211
220
|
with self.access_lock:
|
|
212
221
|
control_data["access-token"] = result.get("access_token")
|
|
213
222
|
duration: int = result.get("expires_in")
|
|
214
|
-
|
|
223
|
+
reserved_claims["exp"] = just_now + duration
|
|
215
224
|
else:
|
|
216
225
|
raise RuntimeError(" - ".join(errors))
|
|
217
226
|
else:
|
|
218
227
|
# JWT service is being provided locally
|
|
219
|
-
|
|
228
|
+
reserved_claims["iat"] = just_now
|
|
229
|
+
reserved_claims["exp"] = just_now + control_data.get("access-max-age")
|
|
230
|
+
claims: dict[str, Any] = item_data.get("public-claims").copy()
|
|
231
|
+
claims.update(reserved_claims)
|
|
232
|
+
claims.update(custom_claims)
|
|
220
233
|
# may raise an exception
|
|
221
234
|
token: str = jwt.encode(payload=claims,
|
|
222
235
|
key=control_data.get("secret-key") or control_data.get("private-key"),
|
|
223
236
|
algorithm=control_data.get("algorithm"))
|
|
224
237
|
with self.access_lock:
|
|
225
238
|
control_data["access-token"] = token
|
|
226
|
-
standard_claims["exp"] = claims.get("exp")
|
|
227
239
|
|
|
228
240
|
# return the token
|
|
229
|
-
diff: timedelta = standard_claims.get("exp") - just_now - timedelta(seconds=10)
|
|
230
241
|
result = {
|
|
231
242
|
"access_token": control_data.get("access-token"),
|
|
232
|
-
"expires_in":
|
|
243
|
+
"expires_in": reserved_claims.get("exp") - just_now
|
|
233
244
|
}
|
|
234
245
|
else:
|
|
235
246
|
# JWT access data not found
|
|
236
|
-
err_msg: str = f"No JWT access data found for '{
|
|
247
|
+
err_msg: str = f"No JWT access data found for '{account_id}'"
|
|
237
248
|
if logger:
|
|
238
249
|
logger.error(err_msg)
|
|
239
250
|
raise RuntimeError(err_msg)
|
|
@@ -275,50 +286,14 @@ class JwtData:
|
|
|
275
286
|
|
|
276
287
|
return result
|
|
277
288
|
|
|
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
289
|
def retrieve_access_data(self,
|
|
311
|
-
|
|
290
|
+
account_id: str,
|
|
312
291
|
logger: Logger = None) -> dict[str, dict[str, Any]]:
|
|
313
292
|
# noinspection HttpUrlsUsage
|
|
314
293
|
"""
|
|
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.
|
|
294
|
+
Retrieve and return the access data in storage for *account_id*.
|
|
320
295
|
|
|
321
|
-
:param
|
|
296
|
+
:param account_id: the account identification
|
|
322
297
|
:param logger: optional logger
|
|
323
298
|
:return: the corresponding item in storage, or *None* if not found
|
|
324
299
|
"""
|
|
@@ -326,19 +301,11 @@ class JwtData:
|
|
|
326
301
|
result: dict[str, dict[str, Any]] | None = None
|
|
327
302
|
|
|
328
303
|
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
|
-
|
|
304
|
+
logger.debug(f"Retrieve access data for account id '{account_id}'")
|
|
335
305
|
# retrieve the data
|
|
336
306
|
with self.access_lock:
|
|
337
307
|
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:
|
|
308
|
+
if account_id == item_data.get("reserved-claims").get("sub"):
|
|
342
309
|
result = item_data
|
|
343
310
|
break
|
|
344
311
|
if logger:
|
|
@@ -350,7 +317,7 @@ class JwtData:
|
|
|
350
317
|
def jwt_request_token(errors: list[str],
|
|
351
318
|
reference_url: str,
|
|
352
319
|
claims: dict[str, Any],
|
|
353
|
-
timeout:
|
|
320
|
+
timeout: int = None,
|
|
354
321
|
logger: Logger = None) -> dict[str, Any]:
|
|
355
322
|
"""
|
|
356
323
|
Obtain and return the JWT token associated with *reference_url*, along with its duration.
|
|
@@ -389,7 +356,7 @@ def jwt_request_token(errors: list[str],
|
|
|
389
356
|
logger.debug(f"JWT token obtained: {result}")
|
|
390
357
|
else:
|
|
391
358
|
# no, report the problem
|
|
392
|
-
err_msg: str = f"POST request
|
|
359
|
+
err_msg: str = f"POST request to '{reference_url}' failed: {response.reason}"
|
|
393
360
|
if response.text:
|
|
394
361
|
err_msg += f" - {response.text}"
|
|
395
362
|
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.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.1
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
|
-
Requires-Dist: pypomes-core>=1.7.
|
|
15
|
+
Requires-Dist: pypomes-core>=1.7.7
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pypomes_jwt/__init__.py,sha256=m0USOMlGVUfofwukykKf6DAPq7CRn4SiY6CeNOOiqJ8,998
|
|
2
|
+
pypomes_jwt/jwt_data.py,sha256=2LnD9_0VMlsUi95jw3biSY5j21boqApSLAyZ_HXMiks,17722
|
|
3
|
+
pypomes_jwt/jwt_pomes.py,sha256=93o0QC7Phsb_29KaLn9mlfE6nUw8HXadqpCZn-Q8gvI,13891
|
|
4
|
+
pypomes_jwt-0.6.1.dist-info/METADATA,sha256=qY2VCQtNpS2WtKUDdMC-Gq5IXyRvFfk8rDhu9MeDyFM,599
|
|
5
|
+
pypomes_jwt-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
pypomes_jwt-0.6.1.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
7
|
+
pypomes_jwt-0.6.1.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
|