pypomes-jwt 1.2.2__py3-none-any.whl → 1.2.4__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/__init__.py CHANGED
@@ -1,5 +1,8 @@
1
1
  from .jwt_config import (
2
- JwtConfig, JwtDbConfig
2
+ JwtConfig, JwtDbConfig, JwtAlgorithm
3
+ )
4
+ from .jwt_external import (
5
+ provider_register, provider_get_token
3
6
  )
4
7
  from .jwt_pomes import (
5
8
  jwt_needed, jwt_verify_request,
@@ -10,7 +13,9 @@ from .jwt_pomes import (
10
13
 
11
14
  __all__ = [
12
15
  # jwt_constants
13
- "JwtConfig", "JwtDbConfig",
16
+ "JwtConfig", "JwtDbConfig", "JwtAlgorithm",
17
+ # jwt_external
18
+ "provider_register", "provider_get_token",
14
19
  # jwt_pomes
15
20
  "jwt_needed", "jwt_verify_request",
16
21
  "jwt_assert_account", "jwt_set_account", "jwt_remove_account",
pypomes_jwt/jwt_config.py CHANGED
@@ -4,19 +4,29 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPubl
4
4
  from enum import Enum, StrEnum
5
5
  from pypomes_core import (
6
6
  APP_PREFIX,
7
- env_get_str, env_get_bytes, env_get_int
7
+ env_get_str, env_get_bytes, env_get_int, env_get_enum
8
8
  )
9
9
  from secrets import token_bytes
10
10
 
11
11
 
12
+ class JwtAlgorithm(StrEnum):
13
+ """
14
+ Supported decoding algorithms.
15
+ """
16
+ HS256 = "HS256"
17
+ HS512 = "HS512"
18
+ RS256 = "RS256"
19
+ RS512 = "RS512"
20
+
21
+
12
22
  # recommended: allow the encode and decode keys to be generated anew when app starts
13
23
  _encoding_key: bytes = env_get_bytes(key=f"{APP_PREFIX}_JWT_ENCODING_KEY",
14
24
  encoding="base64url")
15
25
  _decoding_key: bytes
16
- # one of HS256, HS512, RS256, RS512
17
- _default_algorithm: str = env_get_str(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
18
- def_value="RS256")
19
- if _default_algorithm in ["HS256", "HS512"]:
26
+ _default_algorithm: JwtAlgorithm = env_get_enum(key=f"{APP_PREFIX}_JWT_DEFAULT_ALGORITHM",
27
+ enum_class=JwtAlgorithm,
28
+ def_value=JwtAlgorithm.RS256)
29
+ if _default_algorithm in [JwtAlgorithm.HS256, JwtAlgorithm.HS512]:
20
30
  if not _encoding_key:
21
31
  _encoding_key = token_bytes(nbytes=32)
22
32
  _decoding_key = _encoding_key
@@ -52,12 +62,12 @@ class JwtConfig(Enum):
52
62
 
53
63
  class JwtDbConfig(StrEnum):
54
64
  """
55
- Parameters for JWT databse connection.
65
+ Parameters for JWT database connection.
56
66
  """
57
- ENGINE: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
58
- TABLE: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_TABLE")
59
- COL_ACCOUNT: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ACCOUNT")
60
- COL_ALGORITHM: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ALGORITHM")
61
- COL_DECODER: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_DECODER")
62
- COL_KID: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_KID")
63
- COL_TOKEN: str = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_TOKEN")
67
+ ENGINE = env_get_str(key=f"{APP_PREFIX}_JWT_DB_ENGINE")
68
+ TABLE = env_get_str(key=f"{APP_PREFIX}_JWT_DB_TABLE")
69
+ COL_ACCOUNT = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ACCOUNT")
70
+ COL_ALGORITHM = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_ALGORITHM")
71
+ COL_DECODER = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_DECODER")
72
+ COL_KID = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_KID")
73
+ COL_TOKEN = env_get_str(key=f"{APP_PREFIX}_JWT_DB_COL_TOKEN")
@@ -0,0 +1,127 @@
1
+ import requests
2
+ import sys
3
+ from base64 import b64encode
4
+ from datetime import datetime
5
+ from logging import Logger
6
+ from pypomes_core import TZ_LOCAL, Mimetype, exc_format
7
+ from requests import Response
8
+ from typing import Any
9
+
10
+ # structure:
11
+ # {
12
+ # <provider-id>: {
13
+ # "url": <access-url>,
14
+ # "grant-type": <type-of-grant-to-request>,
15
+ # "user": <basic-auth-user>,
16
+ # "pwd": <basic-auth-pwd>,
17
+ # "token": <auth-token>,
18
+ # "expiration": <timestamp>
19
+ # }
20
+ # }
21
+ _provider_registry: dict[str, dict[str, Any]] = {}
22
+
23
+
24
+ def provider_register(provider_id: str,
25
+ access_url: str,
26
+ grant_type: str,
27
+ auth_user: str,
28
+ auth_pwd: str,
29
+ client_id: str = None,
30
+ use_header: bool = None) -> None:
31
+ """
32
+ Register an external token provider.
33
+
34
+ :param provider_id: the provider's identification
35
+ :param grant_type: the type of grant to request (typically, 'client_credentials' or 'password')
36
+ :param access_url: the url to request tokens with
37
+ :param auth_user: the basic authorization user
38
+ :param auth_pwd: the basic authorization password
39
+ :param client_id: optional client id to add to the request body
40
+ :param use_header: use HTTP header on the request
41
+ """
42
+ global _provider_registry # noqa: PLW0602
43
+ _provider_registry[provider_id] = {
44
+ "url": access_url,
45
+ "grant_type": grant_type,
46
+ "user": auth_user,
47
+ "pwd": auth_pwd,
48
+ "client_id": client_id,
49
+ "use_header": use_header,
50
+ "token": None,
51
+ "expiration": datetime.now(tz=TZ_LOCAL).timestamp()
52
+ }
53
+
54
+
55
+ def provider_get_token(errors: list[str] | None,
56
+ provider_id: str,
57
+ logger: Logger = None) -> str | None:
58
+ """
59
+ Obtain an authentication token from the external provider *provider_id*.
60
+
61
+ :param errors: incidental error messages
62
+ :param provider_id: the provider's identification
63
+ :param logger: optional logger
64
+ """
65
+ # initialize the return variable
66
+ result: str | None = None
67
+
68
+ global _provider_registry # noqa: PLW0602
69
+ err_msg: str | None = None
70
+ provider: dict[str, Any] = _provider_registry.get(provider_id)
71
+ if provider:
72
+ now: float = datetime.now(tz=TZ_LOCAL).timestamp()
73
+ if now > provider.get("expiration"):
74
+ data: dict[str, str] = {"grant_type": provider.get("grant-type")}
75
+ headers: dict[str, str] = {"Content-Type": Mimetype.URLENCODED}
76
+ user: str = provider.get("user")
77
+ pwd: str = provider.get("pwd")
78
+ if provider.get("use_header"):
79
+ enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
80
+ headers["Authorization"] = f"Basic {enc_bytes.decode()}"
81
+ else:
82
+ data["username"] = user
83
+ data["password"] = pwd
84
+ if provider.get("client_id"):
85
+ data["client_id"] = provider.get("client_id")
86
+ url: str = provider.get("url")
87
+ try:
88
+ # typical return on a token request:
89
+ # {
90
+ # "expires_in": <number-of-seconds>,
91
+ # "token_type": "bearer",
92
+ # "access_token": <the-token>
93
+ # }
94
+ response: Response = requests.post(url=url,
95
+ data=data,
96
+ headers=headers,
97
+ timeout=None)
98
+ if response.status_code < 200 or response.status_code >= 300:
99
+ # request resulted in error, report the problem
100
+ err_msg = (f"POST '{url}': failed, "
101
+ f"status {response.status_code}, reason '{response.reason}'")
102
+ else:
103
+ reply: dict[str, Any] = response.json()
104
+ provider["token"] = reply.get("access_token")
105
+ provider["expiration"] = now + int(reply.get("expires_in"))
106
+ if logger:
107
+ logger.debug(msg=f"POST '{url}': status "
108
+ f"{response.status_code}, reason '{response.reason}')")
109
+ except Exception as e:
110
+ # the operation raised an exception
111
+ err_msg = exc_format(exc=e,
112
+ exc_info=sys.exc_info())
113
+ err_msg = f"POST '{url}': error, '{err_msg}'"
114
+ else:
115
+ err_msg: str = f"Provider '{provider_id}' not registered"
116
+
117
+ if err_msg:
118
+ if isinstance(errors, list):
119
+ errors.append(err_msg)
120
+ if logger:
121
+ logger.error(msg=err_msg)
122
+ else:
123
+ result = provider.get("token")
124
+
125
+ return result
126
+
127
+
pypomes_jwt/jwt_pomes.py CHANGED
@@ -23,7 +23,7 @@ def jwt_needed(func: callable) -> callable:
23
23
 
24
24
  :param func: the function being decorated
25
25
  """
26
- # ruff: noqa: ANN003
26
+ # ruff: noqa: ANN003 - Missing type annotation for *{name}
27
27
  def wrapper(*args, **kwargs) -> Response:
28
28
  response: Response = jwt_verify_request(request=request)
29
29
  return response if response else func(*args, **kwargs)
@@ -135,7 +135,7 @@ def jwt_validate_token(errors: list[str] | None,
135
135
  account_id: str = None,
136
136
  logger: Logger = None) -> dict[str, Any] | None:
137
137
  """
138
- Verify if *token* ia a valid JWT token.
138
+ Verify if *token* is a valid JWT token.
139
139
 
140
140
  Attempt to validate non locally issued tokens will not succeed. If *nature* is provided,
141
141
  validate whether *token* is of that nature. A token issued locally has the header claim *kid*
@@ -152,7 +152,7 @@ def jwt_validate_token(errors: list[str] | None,
152
152
  :param nature: prefix identifying the nature of locally issued tokens
153
153
  :param account_id: optionally, validate the token's account owner
154
154
  :param logger: optional logger
155
- :return: The token's claims (*header* and *payload*) if is valid, *None* otherwise
155
+ :return: The token's claims (*header* and *payload*) if it is valid, *None* otherwise
156
156
  """
157
157
  # initialize the return variable
158
158
  result: dict[str, Any] | None = None
@@ -516,7 +516,7 @@ def jwt_get_claims(errors: list[str] | None,
516
516
  token: str,
517
517
  logger: Logger = None) -> dict[str, Any] | None:
518
518
  """
519
- Retrieve and return the claims set of a JWT *token*.
519
+ Retrieve the claims set of a JWT *token*.
520
520
 
521
521
  Any well-constructed JWT token may be provided in *token*, as this operation is not restricted
522
522
  to locally issued tokens. Note that neither the token's signature nor its expiration is verified.
@@ -2,7 +2,7 @@ import jwt
2
2
  import string
3
3
  import sys
4
4
  from base64 import b64encode
5
- from datetime import datetime, timezone
5
+ from datetime import datetime, UTC
6
6
  from logging import Logger
7
7
  from pypomes_core import str_random
8
8
  from pypomes_db import (
@@ -196,7 +196,7 @@ class JwtRegistry:
196
196
  current_claims["jti"] = str_random(size=32,
197
197
  chars=string.ascii_letters + string.digits)
198
198
  current_claims["sub"] = account_id
199
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
199
+ just_now: int = int(datetime.now(tz=UTC).timestamp())
200
200
  current_claims["iat"] = just_now
201
201
  if lead_interval:
202
202
  current_claims["nbf"] = just_now + lead_interval
@@ -249,7 +249,7 @@ class JwtRegistry:
249
249
  current_claims["sub"] = account_id
250
250
  errors: list[str] = []
251
251
 
252
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
252
+ just_now: int = int(datetime.now(tz=UTC).timestamp())
253
253
  current_claims["iat"] = just_now
254
254
  lead_interval = account_data.get("lead-interval")
255
255
  if lead_interval:
@@ -378,10 +378,9 @@ class JwtRegistry:
378
378
  if logger:
379
379
  logger.debug(msg=f"Read {len(recs)} token from storage for account '{account_id}'")
380
380
  # remove the expired tokens
381
- just_now: int = int(datetime.now(tz=timezone.utc).timestamp())
381
+ just_now: int = int(datetime.now(tz=UTC).timestamp())
382
382
  oldest_ts: int = sys.maxsize
383
383
  oldest_id: int | None = None
384
- existing_ids: list[int] = []
385
384
  expired: list[int] = []
386
385
  for rec in recs:
387
386
  token: str = rec[1]
@@ -403,9 +402,6 @@ class JwtRegistry:
403
402
  oldest_ts = iat
404
403
  oldest_id = token_id
405
404
 
406
- # save token id
407
- existing_ids.append(token_id)
408
-
409
405
  # remove expired tokens from persistence
410
406
  if expired:
411
407
  db_delete(errors=errors,
@@ -436,32 +432,27 @@ class JwtRegistry:
436
432
  logger.debug(msg="Oldest active token of account "
437
433
  f"'{account_id}' removed from storage")
438
434
  # persist token
439
- db_insert(errors=errors,
440
- insert_stmt=f"INSERT INTO {JwtDbConfig.TABLE}",
441
- insert_data={
442
- JwtDbConfig.COL_ACCOUNT: account_id,
443
- JwtDbConfig.COL_TOKEN: jwt_token,
444
- JwtDbConfig.COL_ALGORITHM: JwtConfig.DEFAULT_ALGORITHM.value,
445
- JwtDbConfig.COL_DECODER: b64encode(s=JwtConfig.DECODING_KEY.value).decode()
446
- },
447
- engine=DbEngine(JwtDbConfig.ENGINE),
448
- connection=db_conn,
449
- committable=False,
450
- logger=logger)
435
+ col_kid: int = db_insert(errors=errors,
436
+ insert_stmt=f"INSERT INTO {JwtDbConfig.TABLE}",
437
+ insert_data={
438
+ JwtDbConfig.COL_ACCOUNT: account_id,
439
+ JwtDbConfig.COL_TOKEN: jwt_token,
440
+ JwtDbConfig.COL_ALGORITHM: JwtConfig.DEFAULT_ALGORITHM.value,
441
+ JwtDbConfig.COL_DECODER: b64encode(s=JwtConfig.DECODING_KEY.value).decode()
442
+ },
443
+ return_cols={JwtDbConfig.COL_KID: int},
444
+ engine=DbEngine(JwtDbConfig.ENGINE),
445
+ connection=db_conn,
446
+ committable=False,
447
+ logger=logger)
451
448
  if errors:
452
449
  raise RuntimeError("; ".join(errors))
453
450
 
454
451
  # obtain and return the token's storage id
455
- # HAZARD: JWT_DB_COL_TOKEN's column type might prevent it for being used in a WHERE clause
456
- where_clause: str | None = None
457
- if existing_ids:
458
- where_clause = f"{JwtDbConfig.COL_KID} NOT IN {existing_ids}"
459
- where_clause = where_clause.replace("[", "(", 1).replace("]", ")", 1)
460
452
  reply: list[tuple[int]] = db_select(errors=errors,
461
453
  sel_stmt=f"SELECT {JwtDbConfig.COL_KID} "
462
454
  f"FROM {JwtDbConfig.TABLE}",
463
- where_clause=where_clause,
464
- where_data={JwtDbConfig.COL_ACCOUNT: account_id},
455
+ where_data={JwtDbConfig.COL_KID: col_kid},
465
456
  min_count=1,
466
457
  max_count=1,
467
458
  engine=DbEngine(JwtDbConfig.ENGINE),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 1.2.2
3
+ Version: 1.2.4
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
@@ -10,8 +10,8 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
- Requires-Dist: cryptography>=45.0.2
13
+ Requires-Dist: cryptography>=45.0.6
14
14
  Requires-Dist: flask>=3.1.1
15
15
  Requires-Dist: pyjwt>=2.10.1
16
- Requires-Dist: pypomes-core>=2.3.2
17
- Requires-Dist: pypomes-db>=2.2.1
16
+ Requires-Dist: pypomes-core>=2.6.5
17
+ Requires-Dist: pypomes-db>=2.4.8
@@ -0,0 +1,9 @@
1
+ pypomes_jwt/__init__.py,sha256=XS646zYwxnTHHfqnl5ioRN3YGi5QvIpKfrMZcw-grjM,966
2
+ pypomes_jwt/jwt_config.py,sha256=3r9XPWXXAG_wKUs_FDZoTj9j6lmTdNymud_q4E4Opk0,3338
3
+ pypomes_jwt/jwt_external.py,sha256=KYW7V4lL_bY4nVZjB2JvPsgxZFEEQ4ivYCf1JhKUQdw,4974
4
+ pypomes_jwt/jwt_pomes.py,sha256=em_apYg0ptfa5BvobnfXz7BPh_ghPhXGg7zHFK3A8y4,23901
5
+ pypomes_jwt/jwt_registry.py,sha256=pNBpPiR2xINzNnWu_2dcPtRVfXqqPzMVYCXG1HtatUA,22268
6
+ pypomes_jwt-1.2.4.dist-info/METADATA,sha256=QwMXX6r5bS9-2ulkZ-54AIcoGRKVslYWBqx8EWtX_wY,660
7
+ pypomes_jwt-1.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ pypomes_jwt-1.2.4.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
9
+ pypomes_jwt-1.2.4.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=NZzjWKnhjxNuoE32V6soKo9sG5ypmt25V0mBAh3rAIs,793
2
- pypomes_jwt/jwt_config.py,sha256=mtihd58_O00FuFXcNBKsabftG6UHu3Cj24i6cZXoskc,3096
3
- pypomes_jwt/jwt_pomes.py,sha256=e2UUHjsLnLAjNGdSQefHy90xhkwrIcEen63nGR6xc_4,23871
4
- pypomes_jwt/jwt_registry.py,sha256=QJBrqm38N8mZiWi2PDii7jOKL6xdQkYGYmdEv-TA5Rs,22562
5
- pypomes_jwt-1.2.2.dist-info/METADATA,sha256=z0VYKAptePGXxvt4IUQ6iXIaoeH97BGlRUU8YroCRkU,660
6
- pypomes_jwt-1.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- pypomes_jwt-1.2.2.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
- pypomes_jwt-1.2.2.dist-info/RECORD,,