pypomes-jwt 0.9.8__tar.gz → 1.0.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.9.8
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.5
16
- Requires-Dist: pypomes-db>=1.9.7
15
+ Requires-Dist: pypomes-core>=1.8.6
16
+ Requires-Dist: pypomes-db>=1.9.8
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_jwt"
9
- version = "0.9.8"
9
+ version = "1.0.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>=1.8.5",
25
- "pypomes_db>=1.9.7"
24
+ "pypomes_core>=1.8.6",
25
+ "pypomes_db>=1.9.8"
26
26
  ]
27
27
 
28
28
  [project.urls]
@@ -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] = jwt.get_unverified_header(jwt=token)
169
- token_kid: str = token_header.get("kid")
170
- token_alg: str | None = None
171
- token_decoder: bytes | None = None
172
- op_errors: list[str] = []
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
- # retrieve token data from database
175
- if nature and not (token_kid and token_kid[0:1] == nature):
176
- op_errors.append("Invalid token")
177
- elif token_kid and len(token_kid) > 1 and \
178
- token_kid[0:1] in ["A", "R"] and token[1:].isdigit():
179
- # token was likely issued locally
180
- where_data: dict[str, Any] = {JWT_DB_COL_KID: int(token_kid[1:])}
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
- else:
194
- token_alg = JWT_DEFAULT_ALGORITHM
195
- token_decoder = JWT_DECODING_KEY
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
- if not op_errors:
199
- try:
200
- # raises:
201
- # InvalidTokenError: token is invalid
202
- # InvalidKeyError: authentication key is not in the proper format
203
- # ExpiredSignatureError: token and refresh period have expired
204
- # InvalidSignatureError: signature does not match the one provided as part of the token
205
- # ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
206
- # InvalidAlgorithmError: the specified algorithm is not recognized
207
- # InvalidIssuedAtError: 'iat' claim is non-numeric
208
- # MissingRequiredClaimError: a required claim is not contained in the claimset
209
- payload: dict[str, Any] = jwt.decode(jwt=token,
210
- options={
211
- "verify_signature": True,
212
- "verify_exp": True,
213
- "verify_nbf": True
214
- },
215
- key=token_decoder,
216
- require=["iat", "iss", "exp", "sub"],
217
- algorithms=token_alg)
218
- if account_id and payload.get("sub") != account_id:
219
- op_errors.append("Token does not belong to account")
220
- else:
221
- result = {
222
- "header": token_header,
223
- "payload": payload
224
- }
225
- except Exception as e:
226
- op_errors.append(str(e))
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 refresh token of '{account_id}'")
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
- op_errors.append(str(e))
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("; ".join(op_errors))
336
- if isinstance(errors, list):
337
- errors.extend(op_errors)
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
- op_errors.append(str(e))
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=str(e))
537
+ logger.error(msg=f"Error retrieving the token's claimsn: {exc_err}")
510
538
  if isinstance(errors, list):
511
- errors.append(str(e))
539
+ errors.append(exc_err)
512
540
 
513
541
  return result
@@ -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
- oldest: int = sys.maxsize
383
- surplus: int | None = None
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
- token_kid: int = rec[0]
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 < datetime.now(tz=timezone.utc).timestamp():
397
- expired.append(token_kid)
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 < oldest:
402
- oldest = exp
403
- surplus = token_kid
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: surplus},
423
+ where_data={JWT_DB_COL_KID: oldest_id},
423
424
  connection=db_conn,
424
425
  logger=logger)
425
426
  if errors:
File without changes
File without changes
File without changes