pypomes-iam 0.5.6__py3-none-any.whl → 0.5.7__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-iam might be problematic. Click here for more details.
- pypomes_iam/__init__.py +2 -7
- pypomes_iam/iam_common.py +53 -8
- pypomes_iam/iam_services.py +86 -1
- {pypomes_iam-0.5.6.dist-info → pypomes_iam-0.5.7.dist-info}/METADATA +1 -1
- pypomes_iam-0.5.7.dist-info/RECORD +12 -0
- pypomes_iam/iam_pomes.py +0 -82
- pypomes_iam-0.5.6.dist-info/RECORD +0 -13
- {pypomes_iam-0.5.6.dist-info → pypomes_iam-0.5.7.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.6.dist-info → pypomes_iam-0.5.7.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/__init__.py
CHANGED
|
@@ -5,11 +5,8 @@ from .iam_actions import (
|
|
|
5
5
|
from .iam_common import (
|
|
6
6
|
IamServer
|
|
7
7
|
)
|
|
8
|
-
from .iam_pomes import (
|
|
9
|
-
jwt_required
|
|
10
|
-
)
|
|
11
8
|
from .iam_services import (
|
|
12
|
-
logger_register
|
|
9
|
+
jwt_required, logger_register
|
|
13
10
|
)
|
|
14
11
|
from .jusbr_pomes import (
|
|
15
12
|
jusbr_setup, jusbr_get_token
|
|
@@ -30,10 +27,8 @@ __all__ = [
|
|
|
30
27
|
"action_login", "action_logout", "action_token",
|
|
31
28
|
# iam_commons
|
|
32
29
|
"IamServer",
|
|
33
|
-
# iam_pomes
|
|
34
|
-
"jwt_required",
|
|
35
30
|
# iam_services
|
|
36
|
-
"logger_register",
|
|
31
|
+
"jwt_required", "logger_register",
|
|
37
32
|
# jusbr_pomes
|
|
38
33
|
"jusbr_setup", "jusbr_get_token",
|
|
39
34
|
# keycloak_pomes
|
pypomes_iam/iam_common.py
CHANGED
|
@@ -119,12 +119,39 @@ def _get_public_key(iam_server: IamServer,
|
|
|
119
119
|
"""
|
|
120
120
|
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
|
|
123
|
+
containing the public keys used for various purposes, as indicated in the attribute *use*:
|
|
124
|
+
- *enc*: the key is intended for encryption
|
|
125
|
+
- *sig*: the key is intended for digital signature
|
|
126
|
+
- *wrap*: the key is intended for key wrapping
|
|
127
|
+
|
|
128
|
+
A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
|
|
129
|
+
{
|
|
130
|
+
"keys": [
|
|
131
|
+
{
|
|
132
|
+
"kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
|
|
133
|
+
"kty": "RSA",
|
|
134
|
+
"alg": "RSA256",
|
|
135
|
+
"use": "sig",
|
|
136
|
+
"n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
|
|
137
|
+
"e": "AQAB",
|
|
138
|
+
"x5c": [
|
|
139
|
+
"MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
|
|
140
|
+
],
|
|
141
|
+
"x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
|
|
142
|
+
"x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
|
|
143
|
+
},
|
|
144
|
+
...
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
|
|
149
|
+
to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
|
|
123
150
|
|
|
124
151
|
:param iam_server: the reference registered *IAM* server
|
|
125
152
|
:param errors: incidental error messages
|
|
126
153
|
:param logger: optional logger
|
|
127
|
-
:return: the public key in *PEM* format, or *None* if
|
|
154
|
+
:return: the public key in *PEM* format, or *None* if error
|
|
128
155
|
"""
|
|
129
156
|
# initialize the return variable
|
|
130
157
|
result: str | None = None
|
|
@@ -135,9 +162,10 @@ def _get_public_key(iam_server: IamServer,
|
|
|
135
162
|
if registry:
|
|
136
163
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
137
164
|
if now > registry["pk-expiration"]:
|
|
138
|
-
# obtain
|
|
165
|
+
# obtain the JWKS (JSON Web Key Set) from the token issuer
|
|
139
166
|
url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
|
|
140
167
|
if logger:
|
|
168
|
+
logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
|
|
141
169
|
logger.debug(msg=f"GET {url}")
|
|
142
170
|
try:
|
|
143
171
|
response: requests.Response = requests.get(url=url)
|
|
@@ -145,12 +173,29 @@ def _get_public_key(iam_server: IamServer,
|
|
|
145
173
|
# request succeeded
|
|
146
174
|
if logger:
|
|
147
175
|
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
176
|
+
# select the appropriate JWK
|
|
148
177
|
reply: dict[str, Any] = response.json()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
178
|
+
jwk: dict[str, str] | None = None
|
|
179
|
+
# replay["keys"]: list[dict[str, str]]
|
|
180
|
+
for key in reply["keys"]:
|
|
181
|
+
if key.get("use") == "sig":
|
|
182
|
+
jwk = key
|
|
183
|
+
break
|
|
184
|
+
if jwk:
|
|
185
|
+
# convert from 'JWK' to 'PEM' and save it for further use
|
|
186
|
+
result = crypto_jwk_convert(jwk=jwk,
|
|
187
|
+
fmt="PEM")
|
|
188
|
+
registry["public-key"] = result
|
|
189
|
+
lifetime: int = registry["pk-lifetime"] or 0
|
|
190
|
+
registry["pk-expiration"] = now + lifetime if lifetime else sys.maxsize
|
|
191
|
+
if logger:
|
|
192
|
+
logger.debug(f"Public key obtained and saved")
|
|
193
|
+
else:
|
|
194
|
+
msg = "Signature public key missing from the token issuer's JWKS"
|
|
195
|
+
if logger:
|
|
196
|
+
logger.error(msg=msg)
|
|
197
|
+
if isinstance(errors, list):
|
|
198
|
+
errors.append(msg)
|
|
154
199
|
elif logger:
|
|
155
200
|
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
156
201
|
if hasattr(response, "content") and response.content:
|
pypomes_iam/iam_services.py
CHANGED
|
@@ -3,17 +3,102 @@ from flask import Request, Response, request, jsonify
|
|
|
3
3
|
from logging import Logger
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from .iam_common import
|
|
6
|
+
from .iam_common import (
|
|
7
|
+
IamServer, _iam_lock,
|
|
8
|
+
_get_iam_registry, _get_public_key,
|
|
9
|
+
_iam_server_from_endpoint, _iam_server_from_issuer
|
|
10
|
+
)
|
|
7
11
|
from .iam_actions import (
|
|
8
12
|
action_login, action_logout,
|
|
9
13
|
action_token, action_exchange, action_callback
|
|
10
14
|
)
|
|
15
|
+
from .token_pomes import token_get_claims, token_validate
|
|
11
16
|
|
|
12
17
|
# the logger for IAM service operations
|
|
13
18
|
# (used exclusively at the HTTP endpoints - all other functions receive the logger as parameter)
|
|
14
19
|
__IAM_LOGGER: Logger | None = None
|
|
15
20
|
|
|
16
21
|
|
|
22
|
+
def jwt_required(func: callable) -> callable:
|
|
23
|
+
"""
|
|
24
|
+
Create a decorator to authenticate service endpoints with JWT tokens.
|
|
25
|
+
|
|
26
|
+
:param func: the function being decorated
|
|
27
|
+
"""
|
|
28
|
+
# ruff: noqa: ANN003 - Missing type annotation for *{name}
|
|
29
|
+
def wrapper(*args, **kwargs) -> Response:
|
|
30
|
+
response: Response = __request_validate(request=request)
|
|
31
|
+
return response if response else func(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
# prevent a rogue error ("View function mapping is overwriting an existing endpoint function")
|
|
34
|
+
wrapper.__name__ = func.__name__
|
|
35
|
+
|
|
36
|
+
return wrapper
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def __request_validate(request: Request) -> Response:
|
|
40
|
+
"""
|
|
41
|
+
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
42
|
+
|
|
43
|
+
This implementation assumes that HTTP requests are handled with the *Flask* framework.
|
|
44
|
+
Because this code has a high usage frequency, only authentication failures are logged.
|
|
45
|
+
|
|
46
|
+
:param request: the *request* to be verified
|
|
47
|
+
:return: *None* if the *request* is valid, otherwise a *Response* reporting the error
|
|
48
|
+
"""
|
|
49
|
+
# initialize the return variable
|
|
50
|
+
result: Response | None = None
|
|
51
|
+
|
|
52
|
+
# retrieve the authorization from the request header
|
|
53
|
+
auth_header: str = request.headers.get("Authorization")
|
|
54
|
+
|
|
55
|
+
# validate the authorization token
|
|
56
|
+
bad_token: bool = True
|
|
57
|
+
if auth_header and auth_header.startswith("Bearer "):
|
|
58
|
+
# extract and validate the JWT access token
|
|
59
|
+
token: str = auth_header.split(" ")[1]
|
|
60
|
+
claims: dict[str, Any] = token_get_claims(token=token)
|
|
61
|
+
if claims:
|
|
62
|
+
issuer: str = claims["payload"].get("iss")
|
|
63
|
+
recipient_attr: str | None = None
|
|
64
|
+
recipient_id: str = request.values.get("user-id") or request.values.get("login")
|
|
65
|
+
with _iam_lock:
|
|
66
|
+
iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
|
|
67
|
+
errors=None,
|
|
68
|
+
logger=__IAM_LOGGER)
|
|
69
|
+
if iam_server:
|
|
70
|
+
# validate the token's recipient only if a user identification is provided
|
|
71
|
+
if recipient_id:
|
|
72
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
73
|
+
errors=None,
|
|
74
|
+
logger=__IAM_LOGGER)
|
|
75
|
+
if registry:
|
|
76
|
+
recipient_attr = registry["recipient-attr"]
|
|
77
|
+
public_key: str = _get_public_key(iam_server=iam_server,
|
|
78
|
+
errors=None,
|
|
79
|
+
logger=__IAM_LOGGER)
|
|
80
|
+
# validate the token (log errors, only)
|
|
81
|
+
errors: list[str] = []
|
|
82
|
+
if public_key and token_validate(token=token,
|
|
83
|
+
issuer=issuer,
|
|
84
|
+
recipient_id=recipient_id,
|
|
85
|
+
recipient_attr=recipient_attr,
|
|
86
|
+
public_key=public_key,
|
|
87
|
+
errors=errors):
|
|
88
|
+
# token is valid
|
|
89
|
+
bad_token = False
|
|
90
|
+
elif __IAM_LOGGER:
|
|
91
|
+
__IAM_LOGGER.error("; ".join(errors))
|
|
92
|
+
if bad_token and __IAM_LOGGER:
|
|
93
|
+
__IAM_LOGGER.error(f"authorization refused for token {token}")
|
|
94
|
+
|
|
95
|
+
# deny the authorization
|
|
96
|
+
if bad_token:
|
|
97
|
+
result = Response(response="Authorization failed",
|
|
98
|
+
status=401)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
17
102
|
def logger_register(logger: Logger) -> None:
|
|
18
103
|
"""
|
|
19
104
|
Register the logger for HTTP services.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.7
|
|
4
4
|
Summary: A collection of Python pomes, penyeach (IAM modules)
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=GwGK4486tfVD47a4FDiIG6Xl2UeZAKqWUNDauolQNao,1125
|
|
2
|
+
pypomes_iam/iam_actions.py,sha256=0x5kPaDor2rHiOznyF9DLzsNRGLleB66K6RJBPaJkBc,24178
|
|
3
|
+
pypomes_iam/iam_common.py,sha256=yHkbGZb-bSa3sq4UHs1GW4R4474BPTItVm9-J3dd3Bc,12712
|
|
4
|
+
pypomes_iam/iam_services.py,sha256=iq0BQ4sHikPJPiVMv3-q6cYfZVSCxauVkpQilaiSUR8,15783
|
|
5
|
+
pypomes_iam/jusbr_pomes.py,sha256=X_YgY45122tflAzQdAMEcEyVbPvzFigjHLal0qL1v_M,5916
|
|
6
|
+
pypomes_iam/keycloak_pomes.py,sha256=FGdkPjVGEDp5Pwfav4EIc9uSbT4_pG7oPqaiHeJBSLU,6763
|
|
7
|
+
pypomes_iam/provider_pomes.py,sha256=CdEjYjepGXsehn_ujljUQKs0Ws7xNOzBYG6wKp9C7-E,7233
|
|
8
|
+
pypomes_iam/token_pomes.py,sha256=Bz9pT2oU6jTEr_ZEZEJ3kUjH3TfxRyY1_vR319v6CEo,6692
|
|
9
|
+
pypomes_iam-0.5.7.dist-info/METADATA,sha256=tX2E2pV3KcLsdMOH6YJpL6DYQyfdVcg1iA1tI2O5cpk,694
|
|
10
|
+
pypomes_iam-0.5.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
pypomes_iam-0.5.7.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
12
|
+
pypomes_iam-0.5.7.dist-info/RECORD,,
|
pypomes_iam/iam_pomes.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
from flask import Request, Response, request
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from .iam_common import (
|
|
5
|
-
IamServer, _iam_lock, _get_iam_registry,
|
|
6
|
-
_iam_server_from_issuer # _get_public_key
|
|
7
|
-
)
|
|
8
|
-
from .token_pomes import token_get_claims, token_validate
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def jwt_required(func: callable) -> callable:
|
|
12
|
-
"""
|
|
13
|
-
Create a decorator to authenticate service endpoints with JWT tokens.
|
|
14
|
-
|
|
15
|
-
:param func: the function being decorated
|
|
16
|
-
"""
|
|
17
|
-
# ruff: noqa: ANN003 - Missing type annotation for *{name}
|
|
18
|
-
def wrapper(*args, **kwargs) -> Response:
|
|
19
|
-
response: Response = __request_validate(request=request)
|
|
20
|
-
return response if response else func(*args, **kwargs)
|
|
21
|
-
|
|
22
|
-
# prevent a rogue error ("View function mapping is overwriting an existing endpoint function")
|
|
23
|
-
wrapper.__name__ = func.__name__
|
|
24
|
-
|
|
25
|
-
return wrapper
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def __request_validate(request: Request) -> Response:
|
|
29
|
-
"""
|
|
30
|
-
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
31
|
-
|
|
32
|
-
This implementation assumes that HTTP requests are handled with the *Flask* framework.
|
|
33
|
-
|
|
34
|
-
:param request: the *request* to be verified
|
|
35
|
-
:return: *None* if the *request* is valid, otherwise a *Response* reporting the error
|
|
36
|
-
"""
|
|
37
|
-
# initialize the return variable
|
|
38
|
-
result: Response | None = None
|
|
39
|
-
|
|
40
|
-
# retrieve the authorization from the request header
|
|
41
|
-
auth_header: str = request.headers.get("Authorization")
|
|
42
|
-
|
|
43
|
-
# validate the authorization token
|
|
44
|
-
bad_token: bool = True
|
|
45
|
-
if auth_header and auth_header.startswith("Bearer "):
|
|
46
|
-
# extract and validate the JWT access token
|
|
47
|
-
token: str = auth_header.split(" ")[1]
|
|
48
|
-
claims: dict[str, Any] = token_get_claims(token=token)
|
|
49
|
-
if claims:
|
|
50
|
-
issuer: str = claims["payload"].get("iss")
|
|
51
|
-
recipient_attr: str | None = None
|
|
52
|
-
recipient_id: str = request.values.get("user-id") or request.values.get("login")
|
|
53
|
-
with _iam_lock:
|
|
54
|
-
iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
|
|
55
|
-
errors=None,
|
|
56
|
-
logger=None)
|
|
57
|
-
# public_key: str = _get_public_key(iam_server=iam_server,
|
|
58
|
-
# errors=errors,
|
|
59
|
-
# logger=logger)
|
|
60
|
-
public_key = None
|
|
61
|
-
|
|
62
|
-
# validate the token's recipient only if a user identification is provided
|
|
63
|
-
if recipient_id:
|
|
64
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
65
|
-
errors=None,
|
|
66
|
-
logger=None)
|
|
67
|
-
recipient_attr = registry["recipient-attr"]
|
|
68
|
-
|
|
69
|
-
# validate the token
|
|
70
|
-
if token_validate(token=token,
|
|
71
|
-
issuer=issuer,
|
|
72
|
-
recipient_id=recipient_id,
|
|
73
|
-
recipient_attr=recipient_attr,
|
|
74
|
-
public_key=public_key):
|
|
75
|
-
# token is valid
|
|
76
|
-
bad_token = False
|
|
77
|
-
|
|
78
|
-
# deny the authorization
|
|
79
|
-
if bad_token:
|
|
80
|
-
result = Response(response="Authorization failed",
|
|
81
|
-
status=401)
|
|
82
|
-
return result
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
pypomes_iam/__init__.py,sha256=ip9p9-0qCaRPuMVae2JTLZHq6-OPgNKBIL6t6PaSHWg,1180
|
|
2
|
-
pypomes_iam/iam_actions.py,sha256=0x5kPaDor2rHiOznyF9DLzsNRGLleB66K6RJBPaJkBc,24178
|
|
3
|
-
pypomes_iam/iam_common.py,sha256=Xu3Jz-wXzYtEk1hi06lFJ997e9n77I_eeRbpRQ2qCy4,10365
|
|
4
|
-
pypomes_iam/iam_pomes.py,sha256=yA0ZRaD-fp7aZZ-yDnFlh6CvCsEWd-Tf123twQoTPGg,3456
|
|
5
|
-
pypomes_iam/iam_services.py,sha256=ZwSwCiA3XssjG_HgTdkkKtdnQg9UjuqlvFhWVPQfSH8,11871
|
|
6
|
-
pypomes_iam/jusbr_pomes.py,sha256=X_YgY45122tflAzQdAMEcEyVbPvzFigjHLal0qL1v_M,5916
|
|
7
|
-
pypomes_iam/keycloak_pomes.py,sha256=FGdkPjVGEDp5Pwfav4EIc9uSbT4_pG7oPqaiHeJBSLU,6763
|
|
8
|
-
pypomes_iam/provider_pomes.py,sha256=CdEjYjepGXsehn_ujljUQKs0Ws7xNOzBYG6wKp9C7-E,7233
|
|
9
|
-
pypomes_iam/token_pomes.py,sha256=Bz9pT2oU6jTEr_ZEZEJ3kUjH3TfxRyY1_vR319v6CEo,6692
|
|
10
|
-
pypomes_iam-0.5.6.dist-info/METADATA,sha256=ZowLxR_xl3hWHutcz_hGAhvW0B1vsIyIIrrCnwF5uXg,694
|
|
11
|
-
pypomes_iam-0.5.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
-
pypomes_iam-0.5.6.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
13
|
-
pypomes_iam-0.5.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|