pypomes-jwt 1.2.3__py3-none-any.whl → 1.2.5__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,129 @@
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
+ # "client-id": <client-identification>,
18
+ # "use-header": <bool>,
19
+ # "token": <auth-token>,
20
+ # "expiration": <timestamp>
21
+ # }
22
+ # }
23
+ _provider_registry: dict[str, dict[str, Any]] = {}
24
+
25
+
26
+ def provider_register(provider_id: str,
27
+ access_url: str,
28
+ grant_type: str,
29
+ auth_user: str,
30
+ auth_pwd: str,
31
+ client_id: str = None,
32
+ use_header: bool = None) -> None:
33
+ """
34
+ Register an external authentication token provider.
35
+
36
+ :param provider_id: the provider's identification
37
+ :param grant_type: the type of grant to request (typically, 'client_credentials' or 'password')
38
+ :param access_url: the url to request authentication tokens with
39
+ :param auth_user: the basic authorization user
40
+ :param auth_pwd: the basic authorization password
41
+ :param client_id: optional client identification to add to the request data
42
+ :param use_header: add authorization data to HTTP header (defaults to sending them in the body of the request)
43
+ """
44
+ global _provider_registry # noqa: PLW0602
45
+ _provider_registry[provider_id] = {
46
+ "url": access_url,
47
+ "grant_type": grant_type,
48
+ "user": auth_user,
49
+ "pwd": auth_pwd,
50
+ "client-id": client_id,
51
+ "use-header": use_header,
52
+ "token": None,
53
+ "expiration": datetime.now(tz=TZ_LOCAL).timestamp()
54
+ }
55
+
56
+
57
+ def provider_get_token(errors: list[str] | None,
58
+ provider_id: str,
59
+ logger: Logger = None) -> str | None:
60
+ """
61
+ Obtain an authentication token from the external provider *provider_id*.
62
+
63
+ :param errors: incidental error messages
64
+ :param provider_id: the provider's identification
65
+ :param logger: optional logger
66
+ """
67
+ # initialize the return variable
68
+ result: str | None = None
69
+
70
+ global _provider_registry # noqa: PLW0602
71
+ err_msg: str | None = None
72
+ provider: dict[str, Any] = _provider_registry.get(provider_id)
73
+ if provider:
74
+ now: float = datetime.now(tz=TZ_LOCAL).timestamp()
75
+ if now > provider.get("expiration"):
76
+ user: str = provider.get("user")
77
+ pwd: str = provider.get("pwd")
78
+ data: dict[str, str] = {"grant-type": provider.get("grant-type")}
79
+ headers: dict[str, str] = {"Content-Type": Mimetype.URLENCODED}
80
+ if provider.get("use-header"):
81
+ enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
82
+ headers["Authorization"] = f"Basic {enc_bytes.decode()}"
83
+ else:
84
+ data["username"] = user
85
+ data["password"] = pwd
86
+ if provider.get("client-id"):
87
+ data["client-id"] = provider.get("client-id")
88
+ url: str = provider.get("url")
89
+ try:
90
+ # typical return on a token request:
91
+ # {
92
+ # "expires_in": <number-of-seconds>,
93
+ # "token_type": "bearer",
94
+ # "access_token": <the-token>
95
+ # }
96
+ response: Response = requests.post(url=url,
97
+ data=data,
98
+ headers=headers,
99
+ timeout=None)
100
+ if response.status_code < 200 or response.status_code >= 300:
101
+ # request resulted in error, report the problem
102
+ err_msg = (f"POST '{url}': failed, "
103
+ f"status {response.status_code}, reason '{response.reason}'")
104
+ else:
105
+ reply: dict[str, Any] = response.json()
106
+ provider["token"] = reply.get("access_token")
107
+ provider["expiration"] = now + int(reply.get("expires_in"))
108
+ if logger:
109
+ logger.debug(msg=f"POST '{url}': status "
110
+ f"{response.status_code}, reason '{response.reason}')")
111
+ except Exception as e:
112
+ # the operation raised an exception
113
+ err_msg = exc_format(exc=e,
114
+ exc_info=sys.exc_info())
115
+ err_msg = f"POST '{url}': error, '{err_msg}'"
116
+ else:
117
+ err_msg: str = f"Provider '{provider_id}' not registered"
118
+
119
+ if err_msg:
120
+ if isinstance(errors, list):
121
+ errors.append(err_msg)
122
+ if logger:
123
+ logger.error(msg=err_msg)
124
+ else:
125
+ result = provider.get("token")
126
+
127
+ return result
128
+
129
+
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.5
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=tEMsOO3Q-rbWR-6TR_7yapB7Xe9qDVvrL-h8RbxgB2I,5145
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.5.dist-info/METADATA,sha256=Ib7ZJwqF9ZzIENLC27xxddSw-_oPIlnDnftKEVxvs6c,660
7
+ pypomes_jwt-1.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ pypomes_jwt-1.2.5.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
9
+ pypomes_jwt-1.2.5.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,,