pypomes-jwt 1.2.3__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
@@ -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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 1.2.3
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.4
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.4.1
17
- Requires-Dist: pypomes-db>=2.2.9
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=pFqorpjBlnsafn2Ptc-r3z61QZhgaRKbmWKaBsWmq9U,23909
4
- pypomes_jwt/jwt_registry.py,sha256=pNBpPiR2xINzNnWu_2dcPtRVfXqqPzMVYCXG1HtatUA,22268
5
- pypomes_jwt-1.2.3.dist-info/METADATA,sha256=MERUHKMpNm5SI1NjYzfY8eB9GEYJsWYa6mwsQSDgySM,660
6
- pypomes_jwt-1.2.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- pypomes_jwt-1.2.3.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
- pypomes_jwt-1.2.3.dist-info/RECORD,,