pypomes-jwt 1.2.6__tar.gz → 1.2.8__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 1.2.6
3
+ Version: 1.2.8
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
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
13
  Requires-Dist: cryptography>=45.0.6
14
- Requires-Dist: flask>=3.1.1
14
+ Requires-Dist: flask>=3.1.2
15
15
  Requires-Dist: pyjwt>=2.10.1
16
- Requires-Dist: pypomes-core>=2.6.5
17
- Requires-Dist: pypomes-db>=2.4.8
16
+ Requires-Dist: pypomes-core>=2.7.0
17
+ Requires-Dist: pypomes-db>=2.5.8
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_jwt"
9
- version = "1.2.6"
9
+ version = "1.2.8"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -20,10 +20,10 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  "cryptography>=45.0.6",
23
- "Flask>=3.1.1",
23
+ "Flask>=3.1.2",
24
24
  "PyJWT>=2.10.1",
25
- "pypomes_core>=2.6.5",
26
- "pypomes_db>=2.4.8"
25
+ "pypomes_core>=2.7.0",
26
+ "pypomes_db>=2.5.8"
27
27
  ]
28
28
 
29
29
  [project.urls]
@@ -6,7 +6,7 @@ from logging import Logger
6
6
  from pypomes_core import exc_format
7
7
  from pypomes_db import (
8
8
  DbEngine, db_connect, db_commit,
9
- db_rollback, db_select, db_delete
9
+ db_rollback, db_close, db_select, db_delete
10
10
  )
11
11
  from typing import Any
12
12
 
@@ -54,8 +54,7 @@ def jwt_verify_request(request: Request) -> Response:
54
54
  if auth_header and auth_header.startswith("Bearer "):
55
55
  # yes, extract and validate the JWT access token
56
56
  token: str = auth_header.split(" ")[1]
57
- claims: dict[str, Any] = jwt_validate_token(errors=None,
58
- token=token,
57
+ claims: dict[str, Any] = jwt_validate_token(token=token,
59
58
  nature="A")
60
59
  if claims:
61
60
  login: str = request.values.get("login")
@@ -129,10 +128,10 @@ def jwt_remove_account(account_id: str,
129
128
  logger=logger)
130
129
 
131
130
 
132
- def jwt_validate_token(errors: list[str] | None,
133
- token: str,
131
+ def jwt_validate_token(token: str,
134
132
  nature: str = None,
135
133
  account_id: str = None,
134
+ errors: list[str] = None,
136
135
  logger: Logger = None) -> dict[str, Any] | None:
137
136
  """
138
137
  Verify if *token* is a valid JWT token.
@@ -147,10 +146,10 @@ def jwt_validate_token(errors: list[str] | None,
147
146
  On success, return the token's claims (*header* and *payload*), as documented in *jwt_get_claims()*
148
147
  On failure, *errors* will contain the reason(s) for rejecting *token*.
149
148
 
150
- :param errors: incidental error messages
151
149
  :param token: the token to be validated
152
150
  :param nature: prefix identifying the nature of locally issued tokens
153
151
  :param account_id: optionally, validate the token's account owner
152
+ :param errors: incidental error messages
154
153
  :param logger: optional logger
155
154
  :return: The token's claims (*header* and *payload*) if it is valid, *None* otherwise
156
155
  """
@@ -188,12 +187,12 @@ def jwt_validate_token(errors: list[str] | None,
188
187
  where_data: dict[str, Any] = {JwtDbConfig.COL_KID: int(token_kid[1:])}
189
188
  if account_id:
190
189
  where_data[JwtDbConfig.COL_ACCOUNT] = account_id
191
- recs: list[tuple[str]] = db_select(errors=op_errors,
192
- sel_stmt=f"SELECT {JwtDbConfig.COL_ALGORITHM}, "
190
+ recs: list[tuple[str]] = db_select(sel_stmt=f"SELECT {JwtDbConfig.COL_ALGORITHM}, "
193
191
  f"{JwtDbConfig.COL_DECODER} "
194
192
  f"FROM {JwtDbConfig.TABLE}",
195
193
  where_data=where_data,
196
194
  engine=DbEngine(JwtDbConfig.ENGINE),
195
+ errors=op_errors,
197
196
  logger=logger)
198
197
  if recs:
199
198
  token_alg = recs[0][0]
@@ -255,18 +254,18 @@ def jwt_validate_token(errors: list[str] | None,
255
254
  return result
256
255
 
257
256
 
258
- def jwt_revoke_token(errors: list[str] | None,
259
- account_id: str,
257
+ def jwt_revoke_token(account_id: str,
260
258
  token: str,
259
+ errors: list[str] = None,
261
260
  logger: Logger = None) -> bool:
262
261
  """
263
262
  Revoke the *refresh_token* associated with *account_id*.
264
263
 
265
264
  Revoke operations require access to a database table defined by *JWT_DB_TABLE*.
266
265
 
267
- :param errors: incidental error messages
268
266
  :param account_id: the account identification
269
267
  :param token: the token to be revoked
268
+ :param errors: incidental error messages
270
269
  :param logger: optional logger
271
270
  :return: *True* if operation could be performed, *False* otherwise
272
271
  """
@@ -277,22 +276,22 @@ def jwt_revoke_token(errors: list[str] | None,
277
276
  logger.debug(msg=f"Revoking token of account '{account_id}'")
278
277
 
279
278
  op_errors: list[str] = []
280
- token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
281
- token=token,
279
+ token_claims: dict[str, Any] = jwt_validate_token(token=token,
282
280
  account_id=account_id,
281
+ errors=op_errors,
283
282
  logger=logger)
284
283
  if not op_errors:
285
284
  token_kid: str = token_claims["header"].get("kid")
286
285
  if token_kid[0:1] not in ["A", "R"]:
287
286
  op_errors.append("Invalid token")
288
287
  else:
289
- db_delete(errors=op_errors,
290
- delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
288
+ db_delete(delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
291
289
  where_data={
292
290
  JwtDbConfig.COL_KID: int(token_kid[1:]),
293
291
  JwtDbConfig.COL_ACCOUNT: account_id
294
292
  },
295
293
  engine=DbEngine(JwtDbConfig.ENGINE),
294
+ errors=op_errors,
296
295
  logger=logger)
297
296
  if op_errors:
298
297
  if logger:
@@ -305,12 +304,12 @@ def jwt_revoke_token(errors: list[str] | None,
305
304
  return result
306
305
 
307
306
 
308
- def jwt_issue_token(errors: list[str] | None,
309
- account_id: str,
307
+ def jwt_issue_token(account_id: str,
310
308
  nature: str,
311
309
  duration: int,
312
310
  lead_interval: int = None,
313
311
  claims: dict[str, Any] = None,
312
+ errors: list[str] = None,
314
313
  logger: Logger = None) -> str:
315
314
  """
316
315
  Issue or refresh, and return, a JWT token associated with *account_id*, of the specified *nature*.
@@ -320,12 +319,12 @@ def jwt_issue_token(errors: list[str] | None,
320
319
  The parameter *duration* specifies the token's validity interval (at least 60 seconds).
321
320
  These claims are ignored, if specified in *claims*: *iat*, *iss*, *exp*, *jti*, *nbf*, and *sub*.
322
321
 
323
- :param errors: incidental error messages
324
322
  :param account_id: the account identification
325
323
  :param nature: the token's nature, must be a single letter in the range *[B-Z]*, less *R*
326
324
  :param duration: the number of seconds for the token to remain valid (at least 60 seconds)
327
325
  :param claims: optional token's claims
328
326
  :param lead_interval: optional interval for the token to become active (in seconds)
327
+ :param errors: incidental error messages
329
328
  :param logger: optional logger
330
329
  :return: the JWT token data, or *None* if error
331
330
  """
@@ -359,9 +358,9 @@ def jwt_issue_token(errors: list[str] | None,
359
358
  return result
360
359
 
361
360
 
362
- def jwt_issue_tokens(errors: list[str] | None,
363
- account_id: str,
361
+ def jwt_issue_tokens(account_id: str,
364
362
  account_claims: dict[str, Any] = None,
363
+ errors: list[str] = None,
365
364
  logger: Logger = None) -> dict[str, Any]:
366
365
  """
367
366
  Issue the JWT token pair associated with *account_id*, for access and refresh operations.
@@ -377,9 +376,9 @@ def jwt_issue_tokens(errors: list[str] | None,
377
376
  "refresh-token": <jwt-token>
378
377
  }
379
378
 
380
- :param errors: incidental error messages
381
379
  :param account_id: the account identification
382
380
  :param account_claims: if provided, may supercede currently registered account-related claims
381
+ :param errors: incidental error messages
383
382
  :param logger: optional logger
384
383
  :return: the JWT token data, or *None* if error
385
384
  """
@@ -413,9 +412,9 @@ def jwt_issue_tokens(errors: list[str] | None,
413
412
  return result
414
413
 
415
414
 
416
- def jwt_refresh_tokens(errors: list[str] | None,
417
- account_id: str,
415
+ def jwt_refresh_tokens(account_id: str,
418
416
  refresh_token: str,
417
+ errors: list[str] = None,
419
418
  logger: Logger = None) -> dict[str, Any]:
420
419
  """
421
420
  Refresh the JWT token pair associated with *account_id*, for access and refresh operations.
@@ -446,24 +445,23 @@ def jwt_refresh_tokens(errors: list[str] | None,
446
445
  # assert the refresh token
447
446
  if refresh_token:
448
447
  # is the refresh token valid ?
449
- token_claims: dict[str, Any] = jwt_validate_token(errors=op_errors,
450
- token=refresh_token,
448
+ token_claims: dict[str, Any] = jwt_validate_token(token=refresh_token,
451
449
  nature="R",
452
450
  account_id=account_id,
451
+ errors=op_errors,
453
452
  logger=logger)
454
453
  if token_claims:
455
454
  # yes, proceed
456
455
  token_kid: str = token_claims["header"].get("kid")
457
456
 
458
457
  # start the database transaction
459
- db_conn: Any = db_connect(errors=op_errors,
460
- autocommit=False,
458
+ db_conn: Any = db_connect(autocommit=False,
461
459
  engine=DbEngine(JwtDbConfig.ENGINE),
460
+ errors=op_errors,
462
461
  logger=logger)
463
462
  if db_conn:
464
463
  # delete current refresh token
465
- db_delete(errors=op_errors,
466
- delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
464
+ db_delete(delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
467
465
  where_data={
468
466
  JwtDbConfig.COL_KID: int(token_kid[1:]),
469
467
  JwtDbConfig.COL_ACCOUNT: account_id
@@ -471,6 +469,7 @@ def jwt_refresh_tokens(errors: list[str] | None,
471
469
  engine=DbEngine(JwtDbConfig.ENGINE),
472
470
  connection=db_conn,
473
471
  committable=False,
472
+ errors=op_errors,
474
473
  logger=logger)
475
474
 
476
475
  # issue the token pair
@@ -492,13 +491,14 @@ def jwt_refresh_tokens(errors: list[str] | None,
492
491
 
493
492
  # wrap-up the transaction
494
493
  if op_errors:
495
- db_rollback(errors=op_errors,
496
- connection=db_conn,
494
+ db_rollback(connection=db_conn,
497
495
  logger=logger)
498
496
  else:
499
- db_commit(errors=op_errors,
500
- connection=db_conn,
497
+ db_commit(connection=db_conn,
498
+ errors=op_errors,
501
499
  logger=logger)
500
+ db_close(connection=db_conn,
501
+ logger=logger)
502
502
  else:
503
503
  # refresh token not found
504
504
  op_errors.append("Refresh token was not provided")
@@ -512,8 +512,8 @@ def jwt_refresh_tokens(errors: list[str] | None,
512
512
  return result
513
513
 
514
514
 
515
- def jwt_get_claims(errors: list[str] | None,
516
- token: str,
515
+ def jwt_get_claims(token: str,
516
+ errors: list[str] = None,
517
517
  logger: Logger = None) -> dict[str, Any] | None:
518
518
  """
519
519
  Retrieve the claims set of a JWT *token*.
@@ -548,8 +548,8 @@ def jwt_get_claims(errors: list[str] | None,
548
548
  }
549
549
  }
550
550
 
551
- :param errors: incidental error messages
552
551
  :param token: the token to be inspected for claims
552
+ :param errors: incidental error messages
553
553
  :param logger: optional logger
554
554
  :return: the token's claimset, or *None* if error
555
555
  """
@@ -38,8 +38,8 @@ def provider_register(provider_id: str,
38
38
  HTTP Basic Authorization scheme, wherein the credentials are B64-encoded and send in the request headers.
39
39
 
40
40
  Optional constant key-value pairs (such as ['Content-Type', 'application/x-www-form-urlencoded']), to be
41
- added to the request headers, may be specified in *header_data*. Likewise, optional constant key-value pairs
42
- (such as ['grant-type', 'client_crdentials']), to be added to the request body, may be specified in *body_data*.
41
+ added to the request headers, may be specified in *headers_data*. Likewise, optional constant key-value pairs
42
+ (such as ['grant_type', 'client_credentials']), to be added to the request body, may be specified in *body_data*.
43
43
 
44
44
  :param provider_id: the provider's identification
45
45
  :param access_url: the url to request authentication tokens with
@@ -96,9 +96,12 @@ def provider_get_token(errors: list[str] | None,
96
96
  try:
97
97
  # typical return on a token request:
98
98
  # {
99
- # "expires_in": <number-of-seconds>,
100
99
  # "token_type": "bearer",
101
- # "access_token": <the-token>
100
+ # "access_token": <str>,
101
+ # "expires_in": <number-of-seconds>,
102
+ # optional data:
103
+ # "refresh_token": <str>,
104
+ # "refresh_expires_in": <nomber-of-seconds>
102
105
  # }
103
106
  response: Response = requests.post(url=url,
104
107
  data=body_data,
@@ -133,8 +133,7 @@ class JwtRegistry:
133
133
  account_data = self.access_registry.pop(account_id, None)
134
134
 
135
135
  # remove from database
136
- db_delete(errors=None,
137
- delete_stmt=f"DELETE FROM {JwtDbConfig}",
136
+ db_delete(delete_stmt=f"DELETE FROM {JwtDbConfig}",
138
137
  where_data={JwtDbConfig.COL_ACCOUNT: account_id},
139
138
  engine=DbEngine(JwtDbConfig.ENGINE),
140
139
  logger=logger)
@@ -264,9 +263,9 @@ class JwtRegistry:
264
263
  headers={"kid": "R0"})
265
264
 
266
265
  # make sure to have a database connection
267
- curr_conn: Any = db_conn or db_connect(errors=errors,
268
- autocommit=False,
266
+ curr_conn: Any = db_conn or db_connect(autocommit=False,
269
267
  engine=DbEngine(JwtDbConfig.ENGINE),
268
+ errors=errors,
270
269
  logger=logger)
271
270
  if curr_conn:
272
271
  # persist the candidate token (may raise an exception)
@@ -280,24 +279,22 @@ class JwtRegistry:
280
279
  algorithm=JwtConfig.DEFAULT_ALGORITHM.value,
281
280
  headers={"kid": f"R{token_id}"})
282
281
  # persist it
283
- db_update(errors=errors,
284
- update_stmt=f"UPDATE {JwtDbConfig.TABLE}",
282
+ db_update(update_stmt=f"UPDATE {JwtDbConfig.TABLE}",
285
283
  update_data={JwtDbConfig.COL_TOKEN: refresh_token},
286
284
  where_data={JwtDbConfig.COL_KID: token_id},
287
285
  engine=DbEngine(JwtDbConfig.ENGINE),
288
286
  connection=curr_conn,
289
- committable=False,
287
+ errors=errors,
290
288
  logger=logger)
291
289
 
292
290
  # wrap-up the transaction
293
291
  if not db_conn:
294
292
  if errors:
295
- db_rollback(errors=errors,
296
- connection=curr_conn,
293
+ db_rollback(connection=curr_conn,
297
294
  logger=logger)
298
295
  else:
299
- db_commit(errors=errors,
300
- connection=curr_conn,
296
+ db_commit(connection=curr_conn,
297
+ errors=errors,
301
298
  logger=logger)
302
299
  if errors:
303
300
  raise RuntimeError("; ".join(errors))
@@ -340,7 +337,7 @@ class JwtRegistry:
340
337
  @staticmethod
341
338
  def jwt_persist_token(account_id: str,
342
339
  jwt_token: str,
343
- db_conn: Any,
340
+ db_conn: Any = None,
344
341
  logger: Logger = None) -> int:
345
342
  """
346
343
  Persist the given token, making sure that the account limit is complied with.
@@ -349,7 +346,9 @@ class JwtRegistry:
349
346
  If a token's expiration timestamp is in the past, it is removed from storage. If the maximum number
350
347
  of active tokens for *account_id* has been reached, the oldest active one is alse removed,
351
348
  to make room for the new *jwt_token*.
352
- The provided database connection *db_conn* indicates that this operation is part of a larger transaction.
349
+
350
+ If provided, *db_conn* indicates that this operation is part of a larger database transaction.
351
+ Otherwise, the database transaction's scope is limited to this operation.
353
352
 
354
353
  :param account_id: the account identification
355
354
  :param jwt_token: the JWT token to persist
@@ -364,19 +363,18 @@ class JwtRegistry:
364
363
  errors: list[str] = []
365
364
  # noinspection PyTypeChecker
366
365
  recs: list[tuple[int, str, str, str]] = \
367
- db_select(errors=errors,
368
- sel_stmt=f"SELECT {JwtDbConfig.COL_KID}, {JwtDbConfig.COL_TOKEN} "
366
+ db_select(sel_stmt=f"SELECT {JwtDbConfig.COL_KID}, {JwtDbConfig.COL_TOKEN} "
369
367
  f"FROM {JwtDbConfig.TABLE}",
370
368
  where_data={JwtDbConfig.COL_ACCOUNT: account_id},
371
369
  engine=DbEngine(JwtDbConfig.ENGINE),
372
370
  connection=db_conn,
373
- committable=False,
371
+ errors=errors,
374
372
  logger=logger)
375
373
  if errors:
376
374
  raise RuntimeError("; ".join(errors))
377
375
 
378
376
  if logger:
379
- logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
377
+ logger.debug(msg=f"Retrieved {len(recs)} tokens from storage for account '{account_id}'")
380
378
  # remove the expired tokens
381
379
  just_now: int = int(datetime.now(tz=UTC).timestamp())
382
380
  oldest_ts: int = sys.maxsize
@@ -385,8 +383,8 @@ class JwtRegistry:
385
383
  for rec in recs:
386
384
  token: str = rec[1]
387
385
  token_id: int = rec[0]
388
- token_payload: dict[str, Any] = (jwt_get_claims(errors=errors,
389
- token=token,
386
+ token_payload: dict[str, Any] = (jwt_get_claims(token=token,
387
+ errors=errors,
390
388
  logger=logger) or {}).get("payload")
391
389
  if errors:
392
390
  raise RuntimeError("; ".join(errors))
@@ -404,12 +402,11 @@ class JwtRegistry:
404
402
 
405
403
  # remove expired tokens from persistence
406
404
  if expired:
407
- db_delete(errors=errors,
408
- delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
405
+ db_delete(delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
409
406
  where_data={JwtDbConfig.COL_KID: expired},
410
407
  engine=DbEngine(JwtDbConfig.ENGINE),
411
408
  connection=db_conn,
412
- committable=False,
409
+ errors=errors,
413
410
  logger=logger)
414
411
  if errors:
415
412
  raise RuntimeError("; ".join(errors))
@@ -419,12 +416,11 @@ class JwtRegistry:
419
416
 
420
417
  if 0 < JwtConfig.ACCOUNT_LIMIT.value <= len(recs) - len(expired):
421
418
  # delete the oldest token to make way for the new one
422
- db_delete(errors=errors,
423
- delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
419
+ db_delete(delete_stmt=f"DELETE FROM {JwtDbConfig.TABLE}",
424
420
  where_data={JwtDbConfig.COL_KID: oldest_id},
425
421
  engine=DbEngine(JwtDbConfig.ENGINE),
426
422
  connection=db_conn,
427
- committable=False,
423
+ errors=errors,
428
424
  logger=logger)
429
425
  if errors:
430
426
  raise RuntimeError("; ".join(errors))
@@ -432,8 +428,7 @@ class JwtRegistry:
432
428
  logger.debug(msg="Oldest active token of account "
433
429
  f"'{account_id}' removed from storage")
434
430
  # persist token
435
- col_kid: int = db_insert(errors=errors,
436
- insert_stmt=f"INSERT INTO {JwtDbConfig.TABLE}",
431
+ col_kid: int = db_insert(insert_stmt=f"INSERT INTO {JwtDbConfig.TABLE}",
437
432
  insert_data={
438
433
  JwtDbConfig.COL_ACCOUNT: account_id,
439
434
  JwtDbConfig.COL_TOKEN: jwt_token,
@@ -443,14 +438,13 @@ class JwtRegistry:
443
438
  return_cols={JwtDbConfig.COL_KID: int},
444
439
  engine=DbEngine(JwtDbConfig.ENGINE),
445
440
  connection=db_conn,
446
- committable=False,
441
+ errors=errors,
447
442
  logger=logger)
448
443
  if errors:
449
444
  raise RuntimeError("; ".join(errors))
450
445
 
451
446
  # obtain and return the token's storage id
452
- reply: list[tuple[int]] = db_select(errors=errors,
453
- sel_stmt=f"SELECT {JwtDbConfig.COL_KID} "
447
+ reply: list[tuple[int]] = db_select(sel_stmt=f"SELECT {JwtDbConfig.COL_KID} "
454
448
  f"FROM {JwtDbConfig.TABLE}",
455
449
  where_data={JwtDbConfig.COL_KID: col_kid},
456
450
  min_count=1,
@@ -458,6 +452,7 @@ class JwtRegistry:
458
452
  engine=DbEngine(JwtDbConfig.ENGINE),
459
453
  connection=db_conn,
460
454
  committable=False,
455
+ errors=errors,
461
456
  logger=logger)
462
457
  if errors:
463
458
  raise RuntimeError("; ".join(errors))
File without changes
File without changes
File without changes