pypomes-jwt 0.7.6__py3-none-any.whl → 0.7.8__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.

@@ -18,6 +18,7 @@ JWT_DB_CLIENT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_CLIENT") # fo
18
18
  JWT_DB_DRIVER: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_DRIVER") # for SQLServer, only
19
19
  JWT_DB_TABLE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_TABLE")
20
20
  JWT_DB_COL_ACCOUNT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ACCOUNT")
21
+ JWT_DB_COL_HASH: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_HASH")
21
22
  JWT_DB_COL_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_TOKEN")
22
23
  # define the database engine
23
24
  __db_engine: str | None = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
@@ -55,6 +56,7 @@ JWT_REFRESH_MAX_AGE: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_REFRESH_MAX
55
56
  def_value=86400)
56
57
  JWT_ROTATE_TOKENS: Final[bool] = env_get_bool(key=f"{APP_PREFIX}_JWT_ROTATE_TOKENS",
57
58
  def_value=True)
59
+ JWT_ACCOUNT_LIMIT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JWT_ACCOUNT_LIMIT")
58
60
 
59
61
  # recommended: allow the encode and decode keys to be generated anew when app starts
60
62
  __encoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_ENCODE_KEY")
pypomes_jwt/jwt_data.py CHANGED
@@ -1,3 +1,4 @@
1
+ import hashlib
1
2
  import jwt
2
3
  import requests
3
4
  import string
@@ -9,8 +10,9 @@ from threading import Lock
9
10
  from typing import Any
10
11
 
11
12
  from .jwt_constants import (
12
- JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY, JWT_ROTATE_TOKENS,
13
- JWT_DB_ENGINE, JWT_DB_TABLE, JWT_DB_COL_ACCOUNT, JWT_DB_COL_TOKEN
13
+ JWT_DEFAULT_ALGORITHM, JWT_ENCODING_KEY, JWT_DECODING_KEY,
14
+ JWT_ROTATE_TOKENS, JWT_ACCOUNT_LIMIT, JWT_DB_ENGINE,
15
+ JWT_DB_TABLE, JWT_DB_COL_ACCOUNT, JWT_DB_COL_HASH, JWT_DB_COL_TOKEN
14
16
  )
15
17
 
16
18
 
@@ -200,6 +202,7 @@ class JwtData:
200
202
  # obtain new tokens
201
203
  current_claims["jti"] = str_random(size=32,
202
204
  chars=string.ascii_letters + string.digits)
205
+ current_claims["sub"] = account_id
203
206
  current_claims["iss"] = account_data.get("reference-url")
204
207
 
205
208
  # where is the JWT service provider ?
@@ -224,6 +227,8 @@ class JwtData:
224
227
  # JWT service is being provided locally
225
228
  just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
226
229
  current_claims["iat"] = just_now
230
+ if JWT_DEFAULT_ALGORITHM in []:
231
+ current_claims["kid"] = JWT_DECODING_KEY
227
232
 
228
233
  # retrieve the refresh token associated with the account id
229
234
  refresh_token: str | None = None
@@ -257,12 +262,10 @@ class JwtData:
257
262
  algorithm=JWT_DEFAULT_ALGORITHM)
258
263
  # persist the new refresh token
259
264
  if JWT_DB_ENGINE:
260
- from pypomes_db import db_insert
261
- db_insert(errors=errors,
262
- insert_stmt=f"INSERT INTO {JWT_DB_TABLE}",
263
- insert_data={JWT_DB_COL_ACCOUNT: account_id,
264
- JWT_DB_COL_TOKEN: refresh_token},
265
- logger=logger)
265
+ # persist the refresh token
266
+ _jwt_persist_token(account_id=account_id,
267
+ jwt_token=refresh_token,
268
+ logger=logger)
266
269
  if errors:
267
270
  raise RuntimeError(" - ".join(errors))
268
271
 
@@ -340,3 +343,71 @@ def _jwt_request_token(errors: list[str],
340
343
  errors.append(err_msg)
341
344
 
342
345
  return result
346
+
347
+
348
+ def _jwt_persist_token(account_id: str,
349
+ jwt_token: str,
350
+ logger: Logger = None) -> bool:
351
+ """
352
+ Persist the given token, making sure that the account limit is adhered to.
353
+
354
+ :param jwt_token: the JWT token to persist
355
+ :param account_id: the account identification
356
+ :returns: *True* if token was persisted, *False* otherwise
357
+ """
358
+ from pypomes_db import db_select, db_insert, db_delete
359
+ from .jwt_pomes import jwt_get_claims
360
+
361
+ # retrieve the account's tokens
362
+ errors: list[str] = []
363
+ recs: list[tuple[str]] = db_select(errors=errors,
364
+ sel_stmt=f"SELECT {JWT_DB_COL_HASH}, {JWT_DB_COL_TOKEN} FROM {JWT_DB_TABLE} ",
365
+ where_data={JWT_DB_COL_ACCOUNT: f"'{account_id}'"})
366
+ if not errors:
367
+ if logger:
368
+ logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
369
+ # remove the expired tokens
370
+ expired: list[str] = []
371
+ for rec in recs:
372
+ token: str = rec[1]
373
+ token_hash: str = rec[0]
374
+ op_errors: list[str] = []
375
+ token_claims: dict[str, Any] = jwt_get_claims(errors=op_errors,
376
+ token=token,
377
+ validate=False,
378
+ logger=logger)
379
+ if op_errors:
380
+ break
381
+
382
+ exp: int = token_claims["payload"]["exp"]
383
+ if exp < datetime.now(tz=timezone.utc).timestamp():
384
+ expired.append(token_hash)
385
+
386
+ if not errors:
387
+ # remove expired tokens from persistence
388
+ # ruff: noqa: SIM102
389
+ if expired:
390
+ if db_delete(errors=errors,
391
+ delete_stmt=f"DELETE FROM {JWT_DB_TABLE}",
392
+ where_data={
393
+ JWT_DB_COL_ACCOUNT: f"'{account_id}'",
394
+ JWT_DB_COL_HASH: expired
395
+ },
396
+ logger=logger) is not None:
397
+ if logger:
398
+ logger.debug(msg=f"{len(expired)} tokens removed from storage")
399
+ if 0 < JWT_ACCOUNT_LIMIT <= len(recs) - len(expired):
400
+ errors.append("Maximum number of active sessions "
401
+ f"({JWT_ACCOUNT_LIMIT}) exceeded for account '{account_id}'")
402
+ # persist token
403
+ if not errors:
404
+ # ruff: noqa: S324
405
+ hasher = hashlib.new(name="md5")
406
+ hasher.update(jwt_token.encode())
407
+ token_hash: str = hasher.digest().decode()
408
+ db_insert(errors=errors,
409
+ insert_stmt=f"INSERT INTO {JWT_DB_TABLE}",
410
+ insert_data={"ds_hash": token_hash,
411
+ "ds_token": jwt_token})
412
+
413
+ return len(errors) == 0
pypomes_jwt/jwt_pomes.py CHANGED
@@ -31,6 +31,51 @@ def jwt_needed(func: callable) -> callable:
31
31
  return wrapper
32
32
 
33
33
 
34
+ def jwt_verify_request(request: Request,
35
+ logger: Logger = None) -> Response:
36
+ """
37
+ Verify wheher the HTTP *request* has the proper authorization, as per the JWT standard.
38
+
39
+ :param request: the request to be verified
40
+ :param logger: optional logger
41
+ :return: *None* if the request is valid, otherwise a *Response* object reporting the error
42
+ """
43
+ # initialize the return variable
44
+ result: Response | None = None
45
+
46
+ if logger:
47
+ logger.debug(msg="Validate a JWT token")
48
+ err_msg: str | None = None
49
+
50
+ # retrieve the authorization from the request header
51
+ auth_header: str = request.headers.get("Authorization")
52
+
53
+ # was a 'Bearer' authorization obtained ?
54
+ if auth_header and auth_header.startswith("Bearer "):
55
+ # yes, extract and validate the JWT access token
56
+ token: str = auth_header.split(" ")[1]
57
+ if logger:
58
+ logger.debug(msg=f"Token is '{token}'")
59
+ errors: list[str] = []
60
+ jwt_validate_token(errors=errors,
61
+ nature="A",
62
+ token=token)
63
+ if errors:
64
+ err_msg = "; ".join(errors)
65
+ else:
66
+ # no 'Bearer' found, report the error
67
+ err_msg = "Request header has no 'Bearer' data"
68
+
69
+ # log the error and deny the authorization
70
+ if err_msg:
71
+ if logger:
72
+ logger.error(msg=err_msg)
73
+ result = Response(response="Authorization failed",
74
+ status=401)
75
+
76
+ return result
77
+
78
+
34
79
  def jwt_assert_access(account_id: str) -> bool:
35
80
  """
36
81
  Determine whether access for *account_id* has been established.
@@ -247,7 +292,8 @@ def jwt_get_tokens(errors: list[str] | None,
247
292
  if not op_errors:
248
293
  try:
249
294
  result = __jwt_data.issue_tokens(account_id=account_id,
250
- account_claims=account_claims)
295
+ account_claims=account_claims,
296
+ logger=logger)
251
297
  if logger:
252
298
  logger.debug(msg=f"Data is '{result}'")
253
299
  except Exception as e:
@@ -265,12 +311,43 @@ def jwt_get_tokens(errors: list[str] | None,
265
311
 
266
312
  def jwt_get_claims(errors: list[str] | None,
267
313
  token: str,
314
+ validate: bool = True,
268
315
  logger: Logger = None) -> dict[str, Any]:
269
316
  """
270
317
  Obtain and return the claims set of a JWT *token*.
271
318
 
319
+ If *validate* is set to *True*, tha following pieces of information are verified:
320
+ - the token was issued and signed by the local provider, and is not corrupted
321
+ - the claim 'exp' is present and is in the future
322
+ - the claim 'nbf' is present and is in the past
323
+
324
+ Structure of the returned data:
325
+ {
326
+ "header": {
327
+ "alg": "HS256",
328
+ "typ": "JWT"
329
+ },
330
+ "payload": {
331
+ "birthdate": "1980-01-01",
332
+ "email": "jdoe@mail.com",
333
+ "exp": 1516640454,
334
+ "iat": 1516239022,
335
+ "iss": "https://my_id_provider/issue",
336
+ "jti": "Uhsdfgr67FGH567qwSDF33er89retert",
337
+ "gender": "M,
338
+ "name": "John Doe",
339
+ "nbt": 1516249022
340
+ "sub": "1234567890",
341
+ "roles": [
342
+ "administrator",
343
+ "operator"
344
+ ]
345
+ }
346
+ }
347
+
272
348
  :param errors: incidental error messages
273
349
  :param token: the token to be inspected for claims
350
+ :param validate: If *True*, verifies the token's data
274
351
  :param logger: optional logger
275
352
  :return: the token's claimset, or *None* if error
276
353
  """
@@ -281,14 +358,26 @@ def jwt_get_claims(errors: list[str] | None,
281
358
  logger.debug(msg=f"Retrieve claims for token '{token}'")
282
359
 
283
360
  try:
284
- claims: dict[str, Any] = jwt.decode(jwt=token,
285
- options={"verify_signature": False})
286
- if claims.get("nat") in ["A", "R"]:
287
- result = jwt.decode(jwt=token,
288
- key=JWT_DECODING_KEY,
289
- algorithms=[JWT_DEFAULT_ALGORITHM])
361
+ # retrieve the token's payload
362
+ if validate:
363
+ payload: dict[str, Any] = jwt.decode(jwt=token,
364
+ options={
365
+ "verify_signature": True,
366
+ "verify_exp": True,
367
+ "verify_nbf": True
368
+ },
369
+ key=JWT_DECODING_KEY,
370
+ require=["exp", "nbf"],
371
+ algorithms=[JWT_DEFAULT_ALGORITHM])
290
372
  else:
291
- result = claims
373
+ payload: dict[str, Any] = jwt.decode(jwt=token,
374
+ options={"verify_signature": False})
375
+ # retrieve the token's header
376
+ header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
377
+ result = {
378
+ "header": header,
379
+ "payload": payload
380
+ }
292
381
  except Exception as e:
293
382
  if logger:
294
383
  logger.error(msg=str(e))
@@ -296,48 +385,3 @@ def jwt_get_claims(errors: list[str] | None,
296
385
  errors.append(str(e))
297
386
 
298
387
  return result
299
-
300
-
301
- def jwt_verify_request(request: Request,
302
- logger: Logger = None) -> Response:
303
- """
304
- Verify wheher the HTTP *request* has the proper authorization, as per the JWT standard.
305
-
306
- :param request: the request to be verified
307
- :param logger: optional logger
308
- :return: *None* if the request is valid, otherwise a *Response* object reporting the error
309
- """
310
- # initialize the return variable
311
- result: Response | None = None
312
-
313
- if logger:
314
- logger.debug(msg="Validate a JWT token")
315
- err_msg: str | None = None
316
-
317
- # retrieve the authorization from the request header
318
- auth_header: str = request.headers.get("Authorization")
319
-
320
- # was a 'Bearer' authorization obtained ?
321
- if auth_header and auth_header.startswith("Bearer "):
322
- # yes, extract and validate the JWT access token
323
- token: str = auth_header.split(" ")[1]
324
- if logger:
325
- logger.debug(msg=f"Token is '{token}'")
326
- errors: list[str] = []
327
- jwt_validate_token(errors=errors,
328
- nature="A",
329
- token=token)
330
- if errors:
331
- err_msg = "; ".join(errors)
332
- else:
333
- # no 'Bearer' found, report the error
334
- err_msg = "Request header has no 'Bearer' data"
335
-
336
- # log the error and deny the authorization
337
- if err_msg:
338
- if logger:
339
- logger.error(msg=err_msg)
340
- result = Response(response="Authorization failed",
341
- status=401)
342
-
343
- return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 0.7.6
3
+ Version: 0.7.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
@@ -0,0 +1,8 @@
1
+ pypomes_jwt/__init__.py,sha256=dkWeYPNwypjwFuTjx4YtC8QV9ihykF4xcJJ7x86Wc5g,1130
2
+ pypomes_jwt/jwt_constants.py,sha256=8QYpYbpuNfeq15Gwkww2VtEXHFOIkZ1pPS-GU_hifs4,4491
3
+ pypomes_jwt/jwt_data.py,sha256=aZCb6SAgZEz5dFhLWcinr1mc9HZndYQ1gSw5z-5cnCA,19622
4
+ pypomes_jwt/jwt_pomes.py,sha256=nrvu7zIAp0Blq3wAHbvtKLKOVBGlqCttTDOu-LsZxrE,14650
5
+ pypomes_jwt-0.7.8.dist-info/METADATA,sha256=k_n1-ruFPdSXdEHhrAvLDNFoxeXlMAuHZh7Qg5X7Dy0,599
6
+ pypomes_jwt-0.7.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-0.7.8.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-0.7.8.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=dkWeYPNwypjwFuTjx4YtC8QV9ihykF4xcJJ7x86Wc5g,1130
2
- pypomes_jwt/jwt_constants.py,sha256=xUX2raEaDUPJsjAm78lQ0APs4KSs5GYBxSPC5QKrrFE,4327
3
- pypomes_jwt/jwt_data.py,sha256=YH3v8zvOURkB_o0XLMu2y2sFkKCxZBmbyWU5rC2gre4,16419
4
- pypomes_jwt/jwt_pomes.py,sha256=eRQSLA8DVzr9MfVOh5bl6Zz3iOQtRIAQjzIfoz6Fj9o,12915
5
- pypomes_jwt-0.7.6.dist-info/METADATA,sha256=5N0G1VHBXKHiH-QAnUBC9UgeqpjOh9-4-BYMtAl680U,599
6
- pypomes_jwt-0.7.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- pypomes_jwt-0.7.6.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
- pypomes_jwt-0.7.6.dist-info/RECORD,,