pypomes-jwt 1.2.3__tar.gz → 1.2.4__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.
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/PKG-INFO +4 -4
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/pyproject.toml +4 -4
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/src/pypomes_jwt/__init__.py +7 -2
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/src/pypomes_jwt/jwt_config.py +23 -13
- pypomes_jwt-1.2.4/src/pypomes_jwt/jwt_external.py +127 -0
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/src/pypomes_jwt/jwt_pomes.py +3 -3
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/.gitignore +0 -0
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/LICENSE +0 -0
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/README.md +0 -0
- {pypomes_jwt-1.2.3 → pypomes_jwt-1.2.4}/src/pypomes_jwt/jwt_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 1.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.
|
|
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.
|
|
17
|
-
Requires-Dist: pypomes-db>=2.
|
|
16
|
+
Requires-Dist: pypomes-core>=2.6.5
|
|
17
|
+
Requires-Dist: pypomes-db>=2.4.8
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pypomes_jwt"
|
|
9
|
-
version = "1.2.
|
|
9
|
+
version = "1.2.4"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name="GT Nunes", email="wisecoder01@gmail.com" }
|
|
12
12
|
]
|
|
@@ -19,11 +19,11 @@ classifiers = [
|
|
|
19
19
|
"Operating System :: OS Independent"
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
"cryptography>=45.0.
|
|
22
|
+
"cryptography>=45.0.6",
|
|
23
23
|
"Flask>=3.1.1",
|
|
24
24
|
"PyJWT>=2.10.1",
|
|
25
|
-
"pypomes_core>=2.
|
|
26
|
-
"pypomes_db>=2.
|
|
25
|
+
"pypomes_core>=2.6.5",
|
|
26
|
+
"pypomes_db>=2.4.8"
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -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",
|
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if _default_algorithm in [
|
|
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
|
|
65
|
+
Parameters for JWT database connection.
|
|
56
66
|
"""
|
|
57
|
-
ENGINE
|
|
58
|
-
TABLE
|
|
59
|
-
COL_ACCOUNT
|
|
60
|
-
COL_ALGORITHM
|
|
61
|
-
COL_DECODER
|
|
62
|
-
COL_KID
|
|
63
|
-
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
|
+
|
|
@@ -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*
|
|
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
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|