pypomes-jwt 1.1.8__tar.gz → 1.2.0__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-1.1.8 → pypomes_jwt-1.2.0}/PKG-INFO +3 -3
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/pyproject.toml +3 -3
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/src/pypomes_jwt/jwt_pomes.py +13 -7
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/src/pypomes_jwt/jwt_registry.py +135 -135
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/.gitignore +0 -0
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/LICENSE +0 -0
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/README.md +0 -0
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/src/pypomes_jwt/__init__.py +0 -0
- {pypomes_jwt-1.1.8 → pypomes_jwt-1.2.0}/src/pypomes_jwt/jwt_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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,6 +12,6 @@ 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>=2.0.
|
|
16
|
-
Requires-Dist: pypomes-db>=2.1.
|
|
15
|
+
Requires-Dist: pypomes-core>=2.0.6
|
|
16
|
+
Requires-Dist: pypomes-db>=2.1.5
|
|
17
17
|
Requires-Dist: pypomes-logging>=0.6.1
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pypomes_jwt"
|
|
9
|
-
version = "1.
|
|
9
|
+
version = "1.2.0"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name="GT Nunes", email="wisecoder01@gmail.com" }
|
|
12
12
|
]
|
|
@@ -21,8 +21,8 @@ classifiers = [
|
|
|
21
21
|
dependencies = [
|
|
22
22
|
"PyJWT>=2.10.1",
|
|
23
23
|
"cryptography>=44.0.2",
|
|
24
|
-
"pypomes_core>=2.0.
|
|
25
|
-
"pypomes_db>=2.1.
|
|
24
|
+
"pypomes_core>=2.0.6",
|
|
25
|
+
"pypomes_db>=2.1.5",
|
|
26
26
|
"pypomes_logging>=0.6.1"
|
|
27
27
|
]
|
|
28
28
|
|
|
@@ -37,7 +37,9 @@ def jwt_needed(func: callable) -> callable:
|
|
|
37
37
|
|
|
38
38
|
def jwt_verify_request(request: Request) -> Response:
|
|
39
39
|
"""
|
|
40
|
-
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard
|
|
40
|
+
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard..
|
|
41
|
+
|
|
42
|
+
This implementation assumes that HTTP requests are handled with the *Flask* framework.
|
|
41
43
|
|
|
42
44
|
:param request: the *request* to be verified
|
|
43
45
|
:return: *None* if the *request* is valid, otherwise a *Response* reporting the error
|
|
@@ -48,15 +50,19 @@ def jwt_verify_request(request: Request) -> Response:
|
|
|
48
50
|
# retrieve the authorization from the request header
|
|
49
51
|
auth_header: str = request.headers.get("Authorization")
|
|
50
52
|
|
|
51
|
-
#
|
|
53
|
+
# validate the authorization token
|
|
52
54
|
bad_token: bool = True
|
|
53
55
|
if auth_header and auth_header.startswith("Bearer "):
|
|
54
56
|
# yes, extract and validate the JWT access token
|
|
55
57
|
token: str = auth_header.split(" ")[1]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
claims: dict[str, Any] = jwt_validate_token(errors=None,
|
|
59
|
+
token=token,
|
|
60
|
+
nature="A")
|
|
61
|
+
if claims:
|
|
62
|
+
login: str = request.values.get("login")
|
|
63
|
+
subject: str = claims["payload"].get("sub")
|
|
64
|
+
if not login or not subject or login == subject:
|
|
65
|
+
bad_token = False
|
|
60
66
|
|
|
61
67
|
# deny the authorization
|
|
62
68
|
if bad_token:
|
|
@@ -132,7 +138,7 @@ def jwt_validate_token(errors: list[str] | None,
|
|
|
132
138
|
"""
|
|
133
139
|
Verify if *token* ia a valid JWT token.
|
|
134
140
|
|
|
135
|
-
Attempt to validate non locally issued tokens will not succeed.
|
|
141
|
+
Attempt to validate non locally issued tokens will not succeed. If *nature* is provided,
|
|
136
142
|
validate whether *token* is of that nature. A token issued locally has the header claim *kid*
|
|
137
143
|
starting with *A* (for *Access*) or *R* (for *Refresh*), followed by its id in the token database,
|
|
138
144
|
or as a single letter in the range *[B-Z]*, less *R*. If the *kid* claim contains such an id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import jwt
|
|
2
2
|
import string
|
|
3
3
|
import sys
|
|
4
|
-
from base64 import
|
|
4
|
+
from base64 import b64encode
|
|
5
5
|
from datetime import datetime, timezone
|
|
6
6
|
from logging import Logger
|
|
7
7
|
from pypomes_core import str_random
|
|
@@ -184,8 +184,8 @@ class JwtRegistry:
|
|
|
184
184
|
raise RuntimeError(err_msg)
|
|
185
185
|
|
|
186
186
|
# obtain the account data in storage (may raise an exception)
|
|
187
|
-
account_data: dict[str, Any] = self.
|
|
188
|
-
|
|
187
|
+
account_data: dict[str, Any] = self.get_account_data(account_id=account_id,
|
|
188
|
+
logger=logger)
|
|
189
189
|
# issue the token
|
|
190
190
|
current_claims: dict[str, Any] = {}
|
|
191
191
|
iss: str = account_data["claims"].get("iss")
|
|
@@ -240,8 +240,8 @@ class JwtRegistry:
|
|
|
240
240
|
"""
|
|
241
241
|
# process the account data in storage
|
|
242
242
|
with (self.access_lock):
|
|
243
|
-
account_data: dict[str, Any] = self.
|
|
244
|
-
|
|
243
|
+
account_data: dict[str, Any] = self.get_account_data(account_id=account_id,
|
|
244
|
+
logger=logger)
|
|
245
245
|
current_claims: dict[str, Any] = account_data["claims"].copy()
|
|
246
246
|
if account_claims:
|
|
247
247
|
current_claims.update(account_claims)
|
|
@@ -271,10 +271,10 @@ class JwtRegistry:
|
|
|
271
271
|
logger=logger)
|
|
272
272
|
if curr_conn:
|
|
273
273
|
# persist the candidate token (may raise an exception)
|
|
274
|
-
token_id: int =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
token_id: int = JwtRegistry.jwt_persist_token(account_id=account_id,
|
|
275
|
+
jwt_token=refresh_token,
|
|
276
|
+
db_conn=curr_conn,
|
|
277
|
+
logger=logger)
|
|
278
278
|
# issue the definitive refresh token
|
|
279
279
|
refresh_token = jwt.encode(payload=current_claims,
|
|
280
280
|
key=JwtConfig.ENCODING_KEY.value,
|
|
@@ -318,9 +318,9 @@ class JwtRegistry:
|
|
|
318
318
|
"refresh-token": refresh_token
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
def
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
def get_account_data(self,
|
|
322
|
+
account_id: str,
|
|
323
|
+
logger: Logger = PYPOMES_LOGGER) -> dict[str, Any]:
|
|
324
324
|
"""
|
|
325
325
|
Retrieve the JWT access data associated with *account_id*.
|
|
326
326
|
|
|
@@ -338,138 +338,138 @@ class JwtRegistry:
|
|
|
338
338
|
|
|
339
339
|
return result
|
|
340
340
|
|
|
341
|
+
@staticmethod
|
|
342
|
+
def jwt_persist_token(account_id: str,
|
|
343
|
+
jwt_token: str,
|
|
344
|
+
db_conn: Any,
|
|
345
|
+
logger: Logger = PYPOMES_LOGGER) -> int:
|
|
346
|
+
"""
|
|
347
|
+
Persist the given token, making sure that the account limit is complied with.
|
|
341
348
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
sel_stmt=f"SELECT {JwtDbConfig.COL_KID}, {JwtDbConfig.COL_TOKEN} "
|
|
370
|
-
f"FROM {JwtDbConfig.TABLE}",
|
|
371
|
-
where_data={JwtDbConfig.COL_ACCOUNT: account_id},
|
|
372
|
-
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
373
|
-
connection=db_conn,
|
|
374
|
-
committable=False,
|
|
375
|
-
logger=logger)
|
|
376
|
-
if errors:
|
|
377
|
-
raise RuntimeError("; ".join(errors))
|
|
378
|
-
|
|
379
|
-
if logger:
|
|
380
|
-
logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
|
|
381
|
-
# remove the expired tokens
|
|
382
|
-
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
383
|
-
oldest_ts: int = sys.maxsize
|
|
384
|
-
oldest_id: int | None = None
|
|
385
|
-
existing_ids: list[int] = []
|
|
386
|
-
expired: list[int] = []
|
|
387
|
-
for rec in recs:
|
|
388
|
-
token: str = rec[1]
|
|
389
|
-
token_id: int = rec[0]
|
|
390
|
-
token_payload: dict[str, Any] = (jwt_get_claims(errors=errors,
|
|
391
|
-
token=token,
|
|
392
|
-
logger=logger) or {}).get("payload")
|
|
349
|
+
The tokens in storage, associated with *account_id*, are examined for their expiration timestamp.
|
|
350
|
+
If a token's expiration timestamp is in the past, it is removed from storage. If the maximum number
|
|
351
|
+
of active tokens for *account_id* has been reached, the oldest active one is alse removed,
|
|
352
|
+
to make room for the new *jwt_token*.
|
|
353
|
+
The provided database connection *db_conn* indicates that this operation is part of a larger transaction.
|
|
354
|
+
|
|
355
|
+
:param account_id: the account identification
|
|
356
|
+
:param jwt_token: the JWT token to persist
|
|
357
|
+
:param db_conn: the database connection to use
|
|
358
|
+
:param logger: optional logger
|
|
359
|
+
:return: the storage id of the inserted token
|
|
360
|
+
:raises RuntimeError: error accessing the token database
|
|
361
|
+
"""
|
|
362
|
+
from .jwt_pomes import jwt_get_claims
|
|
363
|
+
|
|
364
|
+
# retrieve the account's tokens
|
|
365
|
+
errors: list[str] = []
|
|
366
|
+
# noinspection PyTypeChecker
|
|
367
|
+
recs: list[tuple[int, str, str, str]] = \
|
|
368
|
+
db_select(errors=errors,
|
|
369
|
+
sel_stmt=f"SELECT {JwtDbConfig.COL_KID}, {JwtDbConfig.COL_TOKEN} "
|
|
370
|
+
f"FROM {JwtDbConfig.TABLE}",
|
|
371
|
+
where_data={JwtDbConfig.COL_ACCOUNT: account_id},
|
|
372
|
+
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
373
|
+
connection=db_conn,
|
|
374
|
+
committable=False,
|
|
375
|
+
logger=logger)
|
|
393
376
|
if errors:
|
|
394
377
|
raise RuntimeError("; ".join(errors))
|
|
395
378
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
379
|
+
if logger:
|
|
380
|
+
logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
|
|
381
|
+
# remove the expired tokens
|
|
382
|
+
just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
|
|
383
|
+
oldest_ts: int = sys.maxsize
|
|
384
|
+
oldest_id: int | None = None
|
|
385
|
+
existing_ids: list[int] = []
|
|
386
|
+
expired: list[int] = []
|
|
387
|
+
for rec in recs:
|
|
388
|
+
token: str = rec[1]
|
|
389
|
+
token_id: int = rec[0]
|
|
390
|
+
token_payload: dict[str, Any] = (jwt_get_claims(errors=errors,
|
|
391
|
+
token=token,
|
|
392
|
+
logger=logger) or {}).get("payload")
|
|
393
|
+
if errors:
|
|
394
|
+
raise RuntimeError("; ".join(errors))
|
|
395
|
+
|
|
396
|
+
# find expired tokens
|
|
397
|
+
exp: int = token_payload.get("exp", sys.maxsize)
|
|
398
|
+
if exp < just_now:
|
|
399
|
+
expired.append(token_id)
|
|
400
|
+
|
|
401
|
+
# find oldest token
|
|
402
|
+
iat: int = token_payload.get("iat", sys.maxsize)
|
|
403
|
+
if iat < oldest_ts:
|
|
404
|
+
oldest_ts = iat
|
|
405
|
+
oldest_id = token_id
|
|
406
|
+
|
|
407
|
+
# save token id
|
|
408
|
+
existing_ids.append(token_id)
|
|
409
|
+
|
|
410
|
+
# remove expired tokens from persistence
|
|
411
|
+
if expired:
|
|
412
|
+
db_delete(errors=errors,
|
|
413
|
+
delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
|
|
414
|
+
where_data={JwtDbConfig.COL_KID: expired},
|
|
415
|
+
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
416
|
+
connection=db_conn,
|
|
417
|
+
committable=False,
|
|
418
|
+
logger=logger)
|
|
419
|
+
if errors:
|
|
420
|
+
raise RuntimeError("; ".join(errors))
|
|
421
|
+
if logger:
|
|
422
|
+
logger.debug(msg=f"{len(expired)} tokens of account "
|
|
423
|
+
f"'{account_id}' removed from storage")
|
|
424
|
+
|
|
425
|
+
if 0 < JwtConfig.ACCOUNT_LIMIT.value <= len(recs) - len(expired):
|
|
426
|
+
# delete the oldest token to make way for the new one
|
|
427
|
+
db_delete(errors=errors,
|
|
428
|
+
delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
|
|
429
|
+
where_data={JwtDbConfig.COL_KID: oldest_id},
|
|
430
|
+
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
431
|
+
connection=db_conn,
|
|
432
|
+
committable=False,
|
|
433
|
+
logger=logger)
|
|
434
|
+
if errors:
|
|
435
|
+
raise RuntimeError("; ".join(errors))
|
|
436
|
+
if logger:
|
|
437
|
+
logger.debug(msg="Oldest active token of account "
|
|
438
|
+
f"'{account_id}' removed from storage")
|
|
439
|
+
# persist token
|
|
440
|
+
db_insert(errors=errors,
|
|
441
|
+
insert_stmt=f"INSERT INTO {JwtDbConfig.TABLE}",
|
|
442
|
+
insert_data={
|
|
443
|
+
JwtDbConfig.COL_ACCOUNT: account_id,
|
|
444
|
+
JwtDbConfig.COL_TOKEN: jwt_token,
|
|
445
|
+
JwtDbConfig.COL_ALGORITHM: JwtConfig.DEFAULT_ALGORITHM.value,
|
|
446
|
+
JwtDbConfig.COL_DECODER: b64encode(s=JwtConfig.DECODING_KEY.value).decode()
|
|
447
|
+
},
|
|
415
448
|
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
416
449
|
connection=db_conn,
|
|
417
450
|
committable=False,
|
|
418
451
|
logger=logger)
|
|
419
452
|
if errors:
|
|
420
453
|
raise RuntimeError("; ".join(errors))
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
454
|
+
|
|
455
|
+
# obtain and return the token's storage id
|
|
456
|
+
# HAZARD: JWT_DB_COL_TOKEN's column type might prevent it for being used in a WHERE clause
|
|
457
|
+
where_clause: str | None = None
|
|
458
|
+
if existing_ids:
|
|
459
|
+
where_clause = f"{JwtDbConfig.COL_KID} NOT IN {existing_ids}"
|
|
460
|
+
where_clause = where_clause.replace("[", "(", 1).replace("]", ")", 1)
|
|
461
|
+
reply: list[tuple[int]] = db_select(errors=errors,
|
|
462
|
+
sel_stmt=f"SELECT {JwtDbConfig.COL_KID} "
|
|
463
|
+
f"FROM {JwtDbConfig.TABLE}",
|
|
464
|
+
where_clause=where_clause,
|
|
465
|
+
where_data={JwtDbConfig.COL_ACCOUNT: account_id},
|
|
466
|
+
min_count=1,
|
|
467
|
+
max_count=1,
|
|
468
|
+
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
469
|
+
connection=db_conn,
|
|
470
|
+
committable=False,
|
|
471
|
+
logger=logger)
|
|
434
472
|
if errors:
|
|
435
473
|
raise RuntimeError("; ".join(errors))
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
f"'{account_id}' removed from storage")
|
|
439
|
-
# persist token
|
|
440
|
-
db_insert(errors=errors,
|
|
441
|
-
insert_stmt=f"INSERT INTO {JwtDbConfig.TABLE}",
|
|
442
|
-
insert_data={
|
|
443
|
-
JwtDbConfig.COL_ACCOUNT: account_id,
|
|
444
|
-
JwtDbConfig.COL_TOKEN: jwt_token,
|
|
445
|
-
JwtDbConfig.COL_ALGORITHM: JwtConfig.DEFAULT_ALGORITHM.value,
|
|
446
|
-
JwtDbConfig.COL_DECODER: urlsafe_b64encode(s=JwtConfig.DECODING_KEY.value).decode()
|
|
447
|
-
},
|
|
448
|
-
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
449
|
-
connection=db_conn,
|
|
450
|
-
committable=False,
|
|
451
|
-
logger=logger)
|
|
452
|
-
if errors:
|
|
453
|
-
raise RuntimeError("; ".join(errors))
|
|
454
|
-
|
|
455
|
-
# obtain and return the token's storage id
|
|
456
|
-
# HAZARD: JWT_DB_COL_TOKEN's column type might prevent it for being used in a WHERE clause
|
|
457
|
-
where_clause: str | None = None
|
|
458
|
-
if existing_ids:
|
|
459
|
-
where_clause = f"{JwtDbConfig.COL_KID} NOT IN {existing_ids}"
|
|
460
|
-
where_clause = where_clause.replace("[", "(", 1).replace("]", ")", 1)
|
|
461
|
-
reply: list[tuple[int]] = db_select(errors=errors,
|
|
462
|
-
sel_stmt=f"SELECT {JwtDbConfig.COL_KID} "
|
|
463
|
-
f"FROM {JwtDbConfig.TABLE}",
|
|
464
|
-
where_clause=where_clause,
|
|
465
|
-
where_data={JwtDbConfig.COL_ACCOUNT: account_id},
|
|
466
|
-
min_count=1,
|
|
467
|
-
max_count=1,
|
|
468
|
-
engine=DbEngine(JwtDbConfig.ENGINE),
|
|
469
|
-
connection=db_conn,
|
|
470
|
-
committable=False,
|
|
471
|
-
logger=logger)
|
|
472
|
-
if errors:
|
|
473
|
-
raise RuntimeError("; ".join(errors))
|
|
474
|
-
|
|
475
|
-
return reply[0][0]
|
|
474
|
+
|
|
475
|
+
return reply[0][0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|