pypomes-jwt 0.9.8__py3-none-any.whl → 1.0.0__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/jwt_pomes.py +96 -68
- pypomes_jwt/jwt_registry.py +10 -9
- {pypomes_jwt-0.9.8.dist-info → pypomes_jwt-1.0.0.dist-info}/METADATA +3 -3
- pypomes_jwt-1.0.0.dist-info/RECORD +8 -0
- pypomes_jwt-0.9.8.dist-info/RECORD +0 -8
- {pypomes_jwt-0.9.8.dist-info → pypomes_jwt-1.0.0.dist-info}/WHEEL +0 -0
- {pypomes_jwt-0.9.8.dist-info → pypomes_jwt-1.0.0.dist-info}/licenses/LICENSE +0 -0
pypomes_jwt/jwt_pomes.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import jwt
|
|
2
|
+
import sys
|
|
2
3
|
from base64 import urlsafe_b64decode
|
|
3
4
|
from flask import Request, Response, request
|
|
4
5
|
from logging import Logger
|
|
6
|
+
from pypomes_core import exc_format
|
|
5
7
|
from pypomes_db import db_select, db_delete
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
@@ -152,6 +154,8 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
152
154
|
If the *kid* claim contains such an id, then the cryptographic key needed for validation
|
|
153
155
|
will be obtained from the token database. Otherwise, the current decoding key is used.
|
|
154
156
|
|
|
157
|
+
On success, return the token's claims (header and payload), as documented in *jwt_get_claims()*
|
|
158
|
+
|
|
155
159
|
:param errors: incidental error messages
|
|
156
160
|
:param token: the token to be validated
|
|
157
161
|
:param nature: prefix identifying the nature of locally issued tokens
|
|
@@ -161,74 +165,91 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
161
165
|
"""
|
|
162
166
|
# initialize the return variable
|
|
163
167
|
result: dict[str, Any] | None = None
|
|
168
|
+
|
|
164
169
|
if logger:
|
|
165
170
|
logger.debug(msg="Validate JWT token")
|
|
171
|
+
op_errors: list[str] = []
|
|
166
172
|
|
|
167
173
|
# extract needed data from token header
|
|
168
|
-
token_header: dict[str, Any] =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
token_header: dict[str, Any] | None = None
|
|
175
|
+
try:
|
|
176
|
+
token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
exc_err: str = exc_format(exc=e,
|
|
179
|
+
exc_info=sys.exc_info())
|
|
180
|
+
if logger:
|
|
181
|
+
logger.error(msg=f"Error retrieving the token's header: {exc_err}")
|
|
182
|
+
op_errors.append(exc_err)
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# token
|
|
180
|
-
|
|
181
|
-
if account_id:
|
|
182
|
-
where_data[JWT_DB_COL_ACCOUNT] = account_id
|
|
183
|
-
recs: list[tuple[str]] = db_select(errors=op_errors,
|
|
184
|
-
sel_stmt=f"SELECT {JWT_DB_COL_ALGORITHM}, {JWT_DB_COL_DECODER} "
|
|
185
|
-
f"FROM {JWT_DB_TABLE}",
|
|
186
|
-
where_data=where_data,
|
|
187
|
-
logger=logger)
|
|
188
|
-
if recs:
|
|
189
|
-
token_alg = recs[0][0]
|
|
190
|
-
token_decoder = urlsafe_b64decode(recs[0][1])
|
|
191
|
-
else:
|
|
184
|
+
if not op_errors:
|
|
185
|
+
token_kid: str = token_header.get("kid")
|
|
186
|
+
token_alg: str | None = None
|
|
187
|
+
token_decoder: bytes | None = None
|
|
188
|
+
|
|
189
|
+
# retrieve token data from database
|
|
190
|
+
if nature and not (token_kid and token_kid[0:1] == nature):
|
|
192
191
|
op_errors.append("Invalid token")
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
elif token_kid and len(token_kid) > 1 and \
|
|
193
|
+
token_kid[0:1] in ["A", "R"] and token_kid[1:].isdigit():
|
|
194
|
+
# token was likely issued locally
|
|
195
|
+
where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
|
|
196
|
+
if account_id:
|
|
197
|
+
where_data[JWT_DB_COL_ACCOUNT] = account_id
|
|
198
|
+
recs: list[tuple[str]] = db_select(errors=op_errors,
|
|
199
|
+
sel_stmt=f"SELECT {JWT_DB_COL_ALGORITHM}, {JWT_DB_COL_DECODER} "
|
|
200
|
+
f"FROM {JWT_DB_TABLE}",
|
|
201
|
+
where_data=where_data,
|
|
202
|
+
logger=logger)
|
|
203
|
+
if recs:
|
|
204
|
+
token_alg = recs[0][0]
|
|
205
|
+
token_decoder = urlsafe_b64decode(recs[0][1])
|
|
206
|
+
elif op_errors:
|
|
207
|
+
if logger:
|
|
208
|
+
logger.error(msg=f"Error retrieving the token's decoder: {'; '.join(op_errors)}")
|
|
209
|
+
else:
|
|
210
|
+
if logger:
|
|
211
|
+
logger.error(msg="Token not in the database")
|
|
212
|
+
op_errors.append("Invalid token")
|
|
213
|
+
else:
|
|
214
|
+
token_alg = JWT_DEFAULT_ALGORITHM
|
|
215
|
+
token_decoder = JWT_DECODING_KEY
|
|
196
216
|
|
|
197
217
|
# validate the token
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
218
|
+
if not op_errors:
|
|
219
|
+
try:
|
|
220
|
+
# raises:
|
|
221
|
+
# InvalidTokenError: token is invalid
|
|
222
|
+
# InvalidKeyError: authentication key is not in the proper format
|
|
223
|
+
# ExpiredSignatureError: token and refresh period have expired
|
|
224
|
+
# InvalidSignatureError: signature does not match the one provided as part of the token
|
|
225
|
+
# ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
|
|
226
|
+
# InvalidAlgorithmError: the specified algorithm is not recognized
|
|
227
|
+
# InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
228
|
+
# MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
229
|
+
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
230
|
+
options={
|
|
231
|
+
"verify_signature": True,
|
|
232
|
+
"verify_exp": True,
|
|
233
|
+
"verify_nbf": True
|
|
234
|
+
},
|
|
235
|
+
key=token_decoder,
|
|
236
|
+
require=["iat", "iss", "exp", "sub"],
|
|
237
|
+
algorithms=token_alg)
|
|
238
|
+
if account_id and payload.get("sub") != account_id:
|
|
239
|
+
op_errors.append("Token does not belong to account")
|
|
240
|
+
else:
|
|
241
|
+
result = {
|
|
242
|
+
"header": token_header,
|
|
243
|
+
"payload": payload
|
|
244
|
+
}
|
|
245
|
+
except Exception as e:
|
|
246
|
+
exc_err: str = exc_format(exc=e,
|
|
247
|
+
exc_info=sys.exc_info())
|
|
248
|
+
if logger:
|
|
249
|
+
logger.error(msg=f"Error decoding the token: {exc_err}")
|
|
250
|
+
op_errors.append(exc_err)
|
|
227
251
|
|
|
228
252
|
if op_errors:
|
|
229
|
-
err_msg: str = "; ".join(op_errors)
|
|
230
|
-
if logger:
|
|
231
|
-
logger.error(msg=err_msg)
|
|
232
253
|
if isinstance(errors, list):
|
|
233
254
|
errors.extend(op_errors)
|
|
234
255
|
elif logger:
|
|
@@ -256,7 +277,7 @@ def jwt_revoke_token(errors: list[str] | None,
|
|
|
256
277
|
result: bool = False
|
|
257
278
|
|
|
258
279
|
if logger:
|
|
259
|
-
logger.debug(msg=f"Revoking
|
|
280
|
+
logger.debug(msg=f"Revoking token of account '{account_id}'")
|
|
260
281
|
|
|
261
282
|
op_errors: list[str] = []
|
|
262
283
|
token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
|
|
@@ -328,13 +349,14 @@ def jwt_issue_token(errors: list[str] | None,
|
|
|
328
349
|
logger.debug(msg=f"Token is '{result}'")
|
|
329
350
|
except Exception as e:
|
|
330
351
|
# token issuing failed
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if op_errors:
|
|
352
|
+
exc_err: str = exc_format(exc=e,
|
|
353
|
+
exc_info=sys.exc_info())
|
|
334
354
|
if logger:
|
|
335
|
-
logger.error("
|
|
336
|
-
|
|
337
|
-
|
|
355
|
+
logger.error(msg=f"Error issuing the token: {exc_err}")
|
|
356
|
+
op_errors.append(exc_err)
|
|
357
|
+
|
|
358
|
+
if op_errors and isinstance(errors, list):
|
|
359
|
+
errors.extend(op_errors)
|
|
338
360
|
|
|
339
361
|
return result
|
|
340
362
|
|
|
@@ -378,7 +400,11 @@ def jwt_issue_tokens(errors: list[str] | None,
|
|
|
378
400
|
logger.debug(msg=f"Token data is '{result}'")
|
|
379
401
|
except Exception as e:
|
|
380
402
|
# token issuing failed
|
|
381
|
-
|
|
403
|
+
exc_err: str = exc_format(exc=e,
|
|
404
|
+
exc_info=sys.exc_info())
|
|
405
|
+
if logger:
|
|
406
|
+
logger.error(msg=f"Error issuing the token pair: {exc_err}")
|
|
407
|
+
op_errors.append(exc_err)
|
|
382
408
|
|
|
383
409
|
if op_errors:
|
|
384
410
|
if logger:
|
|
@@ -505,9 +531,11 @@ def jwt_get_claims(errors: list[str] | None,
|
|
|
505
531
|
"payload": payload
|
|
506
532
|
}
|
|
507
533
|
except Exception as e:
|
|
534
|
+
exc_err: str = exc_format(exc=e,
|
|
535
|
+
exc_info=sys.exc_info())
|
|
508
536
|
if logger:
|
|
509
|
-
logger.error(msg=
|
|
537
|
+
logger.error(msg=f"Error retrieving the token's claimsn: {exc_err}")
|
|
510
538
|
if isinstance(errors, list):
|
|
511
|
-
errors.append(
|
|
539
|
+
errors.append(exc_err)
|
|
512
540
|
|
|
513
541
|
return result
|
pypomes_jwt/jwt_registry.py
CHANGED
|
@@ -379,12 +379,13 @@ def _jwt_persist_token(errors: list[str],
|
|
|
379
379
|
if logger:
|
|
380
380
|
logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
|
|
381
381
|
# remove the expired tokens
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
383
|
+
oldest_ts: int = sys.maxsize
|
|
384
|
+
oldest_id: int | None = None
|
|
384
385
|
expired: list[int] = []
|
|
385
386
|
for rec in recs:
|
|
386
387
|
token: str = rec[1]
|
|
387
|
-
|
|
388
|
+
token_id: int = rec[0]
|
|
388
389
|
token_claims: dict[str, Any] = jwt_get_claims(errors=errors,
|
|
389
390
|
token=token,
|
|
390
391
|
logger=logger)
|
|
@@ -393,14 +394,14 @@ def _jwt_persist_token(errors: list[str],
|
|
|
393
394
|
|
|
394
395
|
# find expired tokens
|
|
395
396
|
exp: int = token_claims["payload"].get("exp", sys.maxsize)
|
|
396
|
-
if exp <
|
|
397
|
-
expired.append(
|
|
397
|
+
if exp < just_now:
|
|
398
|
+
expired.append(token_id)
|
|
398
399
|
|
|
399
400
|
# find oldest token
|
|
400
401
|
iat: int = token_claims["payload"].get("iat", sys.maxsize)
|
|
401
|
-
if iat <
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
if iat < oldest_ts:
|
|
403
|
+
oldest_ts = exp
|
|
404
|
+
oldest_id = token_id
|
|
404
405
|
|
|
405
406
|
# remove expired tokens from persistence
|
|
406
407
|
if expired:
|
|
@@ -419,7 +420,7 @@ def _jwt_persist_token(errors: list[str],
|
|
|
419
420
|
# delete the oldest token to make way for the new one
|
|
420
421
|
db_delete(errors=errors,
|
|
421
422
|
delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
|
|
422
|
-
where_data={JWT_DB_COL_KID:
|
|
423
|
+
where_data={JWT_DB_COL_KID: oldest_id},
|
|
423
424
|
connection=db_conn,
|
|
424
425
|
logger=logger)
|
|
425
426
|
if errors:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
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
|
|
@@ -12,5 +12,5 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Requires-Dist: cryptography>=44.0.2
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
|
-
Requires-Dist: pypomes-core>=1.8.
|
|
16
|
-
Requires-Dist: pypomes-db>=1.9.
|
|
15
|
+
Requires-Dist: pypomes-core>=1.8.6
|
|
16
|
+
Requires-Dist: pypomes-db>=1.9.8
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pypomes_jwt/__init__.py,sha256=fLr_M8yXlcmSTNPMdJOJQlMmtaiK5YKh0vKjOp3z2E4,1446
|
|
2
|
+
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
+
pypomes_jwt/jwt_pomes.py,sha256=zlMxCROxSLn-yLBRkoTQpiFsqQLJcjIXQEPMgePpkXc,21622
|
|
4
|
+
pypomes_jwt/jwt_registry.py,sha256=5L6n4ue7YEkGT60U69WvoobKR9ziHkqdIgeOmYEn2gU,21472
|
|
5
|
+
pypomes_jwt-1.0.0.dist-info/METADATA,sha256=jqAmlWUMTREyqVBQo5r_sG0LmatnBzM9Ze95VqARuiM,632
|
|
6
|
+
pypomes_jwt-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
pypomes_jwt-1.0.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
+
pypomes_jwt-1.0.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pypomes_jwt/__init__.py,sha256=fLr_M8yXlcmSTNPMdJOJQlMmtaiK5YKh0vKjOp3z2E4,1446
|
|
2
|
-
pypomes_jwt/jwt_constants.py,sha256=IQV39AiZKGuU8XxZBgJ-KJZQZ_mmnxyOnRZeuxlqDRk,4045
|
|
3
|
-
pypomes_jwt/jwt_pomes.py,sha256=_aK1TjV4cJ96ERa3VzloU9VJZVktbee2QdBTMYxR1DE,20064
|
|
4
|
-
pypomes_jwt/jwt_registry.py,sha256=mASF2NxHK4jWNswqfDXPGGuv8RsSIvLh1MzldaBxdeA,21425
|
|
5
|
-
pypomes_jwt-0.9.8.dist-info/METADATA,sha256=q37-GtefddZ2WqtcHMh8wGtK8usua2to3g7w5aO5-Jc,632
|
|
6
|
-
pypomes_jwt-0.9.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
-
pypomes_jwt-0.9.8.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
|
|
8
|
-
pypomes_jwt-0.9.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|