pypomes-iam 0.8.6__tar.gz → 0.9.3__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-iam might be problematic. Click here for more details.
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/PKG-INFO +2 -2
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/pyproject.toml +2 -2
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/pypomes_iam/__init__.py +0 -5
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/pypomes_iam/iam_actions.py +19 -19
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/pypomes_iam/iam_common.py +15 -83
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/pypomes_iam/iam_pomes.py +1 -1
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/pypomes_iam/iam_services.py +41 -39
- pypomes_iam-0.8.6/src/pypomes_iam/token_pomes.py +0 -183
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/.gitignore +0 -0
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/LICENSE +0 -0
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/README.md +0 -0
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/__init__.py +0 -0
- {pypomes_iam-0.8.6 → pypomes_iam-0.9.3}/src/pypomes_iam/provider_pomes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.3
|
|
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
|
|
@@ -13,5 +13,5 @@ Requires-Python: >=3.12
|
|
|
13
13
|
Requires-Dist: flask>=3.1.2
|
|
14
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
15
15
|
Requires-Dist: pypomes-core>=2.8.6
|
|
16
|
-
Requires-Dist: pypomes-crypto>=0.
|
|
16
|
+
Requires-Dist: pypomes-crypto>=0.5.0
|
|
17
17
|
Requires-Dist: requests>=2.32.5
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pypomes_iam"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.9.3"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name="GT Nunes", email="wisecoder01@gmail.com" }
|
|
12
12
|
]
|
|
@@ -22,7 +22,7 @@ dependencies = [
|
|
|
22
22
|
"Flask>=3.1.2",
|
|
23
23
|
"PyJWT>=2.10.1",
|
|
24
24
|
"pypomes-core>=2.8.6",
|
|
25
|
-
"pypomes-crypto>=0.
|
|
25
|
+
"pypomes-crypto>=0.5.0",
|
|
26
26
|
"requests>=2.32.5"
|
|
27
27
|
]
|
|
28
28
|
|
|
@@ -19,9 +19,6 @@ from .provider_pomes import (
|
|
|
19
19
|
service_get_token, provider_get_token,
|
|
20
20
|
iam_setup_provider, provider_setup_endpoint, provider_setup_logger
|
|
21
21
|
)
|
|
22
|
-
from .token_pomes import (
|
|
23
|
-
token_get_claims, token_get_values, token_validate
|
|
24
|
-
)
|
|
25
22
|
|
|
26
23
|
__all__ = [
|
|
27
24
|
# iam_actions
|
|
@@ -40,8 +37,6 @@ __all__ = [
|
|
|
40
37
|
"IamProvider", "ProviderParam",
|
|
41
38
|
"service_get_token", "provider_get_token",
|
|
42
39
|
"iam_setup_provider", "provider_setup_endpoint", "provider_setup_logger",
|
|
43
|
-
# token_pomes
|
|
44
|
-
"token_get_claims", "token_get_values", "token_validate"
|
|
45
40
|
]
|
|
46
41
|
|
|
47
42
|
from importlib.metadata import version
|
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from logging import Logger
|
|
8
8
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
9
|
+
from pypomes_crypto import jwt_get_values, jwt_validate
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from .iam_common import (
|
|
@@ -13,7 +14,6 @@ from .iam_common import (
|
|
|
13
14
|
_get_iam_users, _get_iam_registry, _get_public_key,
|
|
14
15
|
_get_login_timeout, _get_user_data, _iam_server_from_issuer
|
|
15
16
|
)
|
|
16
|
-
from .token_pomes import token_get_values, token_validate
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def iam_login(iam_server: IamServer,
|
|
@@ -56,7 +56,7 @@ def iam_login(iam_server: IamServer,
|
|
|
56
56
|
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
57
57
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
58
58
|
if target_idp:
|
|
59
|
-
oauth_state += f"
|
|
59
|
+
oauth_state += f"_{target_idp}"
|
|
60
60
|
|
|
61
61
|
with _iam_lock:
|
|
62
62
|
# retrieve the user data from the IAM server's registry
|
|
@@ -322,8 +322,8 @@ def iam_callback(iam_server: IamServer,
|
|
|
322
322
|
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
323
323
|
errors.append("Operation timeout")
|
|
324
324
|
else:
|
|
325
|
-
pos: int = oauth_state.rfind("
|
|
326
|
-
target_idp: str = oauth_state[pos+
|
|
325
|
+
pos: int = oauth_state.rfind("_")
|
|
326
|
+
target_idp: str = oauth_state[pos+1:] if pos > 0 else None
|
|
327
327
|
target_iam: IamServer = IamServer(target_idp) if target_idp in IamServer else None
|
|
328
328
|
target_data: dict[str, Any] = user_data.copy() if target_iam else None
|
|
329
329
|
users.pop(oauth_state)
|
|
@@ -421,10 +421,10 @@ def iam_exchange(iam_server: IamServer,
|
|
|
421
421
|
|
|
422
422
|
# obtain the token to be exchanged
|
|
423
423
|
token: str = args.get("access-token") if user_id else None
|
|
424
|
-
token_issuer: tuple[str] =
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
424
|
+
token_issuer: tuple[str] = jwt_get_values(token=token,
|
|
425
|
+
keys=("iss",),
|
|
426
|
+
errors=errors,
|
|
427
|
+
logger=logger)
|
|
428
428
|
if not errors:
|
|
429
429
|
with _iam_lock:
|
|
430
430
|
# retrieve the IAM server's registry
|
|
@@ -604,10 +604,10 @@ def __assert_link(iam_server: IamServer,
|
|
|
604
604
|
break
|
|
605
605
|
if no_link:
|
|
606
606
|
# link the identities
|
|
607
|
-
token_sub: tuple[str] =
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
607
|
+
token_sub: tuple[str] = jwt_get_values(token=token,
|
|
608
|
+
keys=("sub",),
|
|
609
|
+
errors=errors,
|
|
610
|
+
logger=logger)
|
|
611
611
|
if token_sub:
|
|
612
612
|
if logger:
|
|
613
613
|
logger.debug(msg="Creating an association between identifications "
|
|
@@ -1023,13 +1023,13 @@ def __validate_and_store(iam_server: IamServer,
|
|
|
1023
1023
|
recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
|
|
1024
1024
|
login_id = user_data.pop("login-id", None)
|
|
1025
1025
|
base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
|
|
1026
|
-
claims: dict[str, dict[str, Any]] =
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1026
|
+
claims: dict[str, dict[str, Any]] = jwt_validate(token=token,
|
|
1027
|
+
issuer=base_url,
|
|
1028
|
+
recipient_id=login_id,
|
|
1029
|
+
recipient_attr=recipient_attr,
|
|
1030
|
+
public_key=public_key,
|
|
1031
|
+
errors=errors,
|
|
1032
|
+
logger=logger)
|
|
1033
1033
|
if claims:
|
|
1034
1034
|
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
1035
1035
|
errors=errors,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import requests
|
|
2
1
|
import sys
|
|
3
2
|
from datetime import datetime
|
|
4
3
|
from enum import StrEnum
|
|
5
4
|
from logging import Logger
|
|
6
5
|
from pypomes_core import (
|
|
7
|
-
APP_PREFIX, TZ_LOCAL,
|
|
6
|
+
APP_PREFIX, TZ_LOCAL,
|
|
8
7
|
env_get_int, env_get_str, env_get_strs
|
|
9
8
|
)
|
|
10
|
-
from pypomes_crypto import
|
|
9
|
+
from pypomes_crypto import jwt_get_public_key
|
|
11
10
|
from threading import RLock
|
|
12
11
|
from typing import Any, Final
|
|
13
12
|
|
|
@@ -203,7 +202,7 @@ def _iam_server_from_issuer(issuer: str,
|
|
|
203
202
|
for iam_server, registry in _IAM_SERVERS.items():
|
|
204
203
|
base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
|
|
205
204
|
if base_url == issuer:
|
|
206
|
-
result =
|
|
205
|
+
result = IamServer(iam_server)
|
|
207
206
|
break
|
|
208
207
|
|
|
209
208
|
if not result:
|
|
@@ -222,34 +221,7 @@ def _get_public_key(iam_server: IamServer,
|
|
|
222
221
|
"""
|
|
223
222
|
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
224
223
|
|
|
225
|
-
|
|
226
|
-
containing the public keys used for various purposes, as indicated in the attribute *use*:
|
|
227
|
-
- *enc*: the key is intended for encryption
|
|
228
|
-
- *sig*: the key is intended for digital signature
|
|
229
|
-
- *wrap*: the key is intended for key wrapping
|
|
230
|
-
|
|
231
|
-
A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
|
|
232
|
-
{
|
|
233
|
-
"keys": [
|
|
234
|
-
{
|
|
235
|
-
"kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
|
|
236
|
-
"kty": "RSA",
|
|
237
|
-
"alg": "RSA256",
|
|
238
|
-
"use": "sig",
|
|
239
|
-
"n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
|
|
240
|
-
"e": "AQAB",
|
|
241
|
-
"x5c": [
|
|
242
|
-
"MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
|
|
243
|
-
],
|
|
244
|
-
"x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
|
|
245
|
-
"x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
|
|
246
|
-
},
|
|
247
|
-
...
|
|
248
|
-
]
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
|
|
252
|
-
to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
|
|
224
|
+
The signaature is obtained and stored in *PEM* (Privacy-Enhanced Mail) format.
|
|
253
225
|
|
|
254
226
|
:param iam_server: the reference registered *IAM* server
|
|
255
227
|
:param errors: incidental error messages
|
|
@@ -265,57 +237,17 @@ def _get_public_key(iam_server: IamServer,
|
|
|
265
237
|
if registry:
|
|
266
238
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
267
239
|
if now > registry[ServerParam.PK_EXPIRATION]:
|
|
268
|
-
# obtain the
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
280
|
-
# select the appropriate JWK
|
|
281
|
-
reply: dict[str, list[dict[str, str]]] = response.json()
|
|
282
|
-
jwk: dict[str, str] | None = None
|
|
283
|
-
for key in reply["keys"]:
|
|
284
|
-
if key.get("use") == "sig":
|
|
285
|
-
jwk = key
|
|
286
|
-
break
|
|
287
|
-
if jwk:
|
|
288
|
-
# convert from 'JWK' to 'PEM' and save it for further use
|
|
289
|
-
result = crypto_jwk_convert(jwk=jwk,
|
|
290
|
-
fmt="PEM")
|
|
291
|
-
registry[ServerParam.PUBLIC_KEY] = result
|
|
292
|
-
lifetime: int = registry[ServerParam.PK_LIFETIME] or 0
|
|
293
|
-
registry[ServerParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
|
|
294
|
-
if logger:
|
|
295
|
-
logger.debug("Public key obtained and saved")
|
|
296
|
-
else:
|
|
297
|
-
msg = "Signature public key missing from the token issuer's JWKS"
|
|
298
|
-
if logger:
|
|
299
|
-
logger.error(msg=msg)
|
|
300
|
-
if isinstance(errors, list):
|
|
301
|
-
errors.append(msg)
|
|
302
|
-
elif logger:
|
|
303
|
-
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
304
|
-
if hasattr(response, "content") and response.content:
|
|
305
|
-
msg += f", content {response.content}"
|
|
306
|
-
logger.error(msg=msg)
|
|
307
|
-
if isinstance(errors, list):
|
|
308
|
-
errors.append(msg)
|
|
309
|
-
except Exception as e:
|
|
310
|
-
# the operation raised an exception
|
|
311
|
-
msg = exc_format(exc=e,
|
|
312
|
-
exc_info=sys.exc_info())
|
|
313
|
-
if logger:
|
|
314
|
-
logger.error(msg=msg)
|
|
315
|
-
if isinstance(errors, list):
|
|
316
|
-
errors.append(msg)
|
|
317
|
-
else:
|
|
318
|
-
result = registry[ServerParam.PUBLIC_KEY]
|
|
240
|
+
# obtain the public key from the token issuer
|
|
241
|
+
issuer: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
|
|
242
|
+
registry[ServerParam.PUBLIC_KEY] = jwt_get_public_key(issuer=issuer,
|
|
243
|
+
fmt="PEM",
|
|
244
|
+
errors=errors,
|
|
245
|
+
logger=logger)
|
|
246
|
+
lifetime: int = registry[ServerParam.PK_LIFETIME] or 0
|
|
247
|
+
registry[ServerParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
|
|
248
|
+
|
|
249
|
+
if not errors:
|
|
250
|
+
result = registry[ServerParam.PUBLIC_KEY]
|
|
319
251
|
|
|
320
252
|
return result
|
|
321
253
|
|
|
@@ -131,7 +131,7 @@ def iam_setup_endpoints(flask_app: Flask,
|
|
|
131
131
|
if "callback_exchange_endpoint" in defaulted_params:
|
|
132
132
|
callback_exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK_EXCHANGE")
|
|
133
133
|
if "exchange_endpoint" in defaulted_params:
|
|
134
|
-
|
|
134
|
+
exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
|
|
135
135
|
if "login_endpoint" in defaulted_params:
|
|
136
136
|
login_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN")
|
|
137
137
|
if "logout_endpoint" in defaulted_params:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from flask import Request, Response, request, jsonify
|
|
3
|
+
from pypomes_crypto import jwt_get_values, jwt_validate
|
|
3
4
|
from logging import Logger
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -12,7 +13,6 @@ from .iam_actions import (
|
|
|
12
13
|
iam_login, iam_logout, iam_callback,
|
|
13
14
|
iam_exchange, iam_get_token, iam_userinfo
|
|
14
15
|
)
|
|
15
|
-
from .token_pomes import token_get_claims, token_validate
|
|
16
16
|
|
|
17
17
|
# the logger for IAM service operations
|
|
18
18
|
# (used exclusively at the HTTP endpoints - all other functions receive the logger as parameter)
|
|
@@ -56,36 +56,38 @@ def __request_validate(request: Request) -> Response:
|
|
|
56
56
|
bad_token: bool = True
|
|
57
57
|
token: str = __get_bearer_token(request=request)
|
|
58
58
|
if token:
|
|
59
|
+
# errors list for error loging, only
|
|
60
|
+
errors: list[str] = []
|
|
59
61
|
# extract token claims
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
(issuer,) = jwt_get_values(token=token,
|
|
63
|
+
keys=("iss",))
|
|
64
|
+
public_key: str | None = None
|
|
65
|
+
recipient_attr: str | None = None
|
|
66
|
+
recipient_id: str = (request.values.get("user-id") or request.values.get("login") or
|
|
67
|
+
(request.get_json(silent=True) or {}).get("user-id") or
|
|
68
|
+
(request.get_json(silent=True) or {}).get("login"))
|
|
69
|
+
with _iam_lock:
|
|
70
|
+
iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
|
|
71
|
+
errors=None,
|
|
72
|
+
logger=__IAM_LOGGER)
|
|
73
|
+
if iam_server:
|
|
74
|
+
# validate the token's recipient only if a user identification is provided
|
|
75
|
+
if recipient_id:
|
|
76
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
77
|
+
errors=None,
|
|
78
|
+
logger=__IAM_LOGGER)
|
|
79
|
+
if registry:
|
|
80
|
+
recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
|
|
81
|
+
public_key = _get_public_key(iam_server=iam_server,
|
|
82
|
+
errors=None,
|
|
83
|
+
logger=__IAM_LOGGER)
|
|
84
|
+
# validate the token
|
|
85
|
+
if jwt_validate(token=token,
|
|
86
|
+
issuer=issuer,
|
|
87
|
+
recipient_id=recipient_id,
|
|
88
|
+
recipient_attr=recipient_attr,
|
|
89
|
+
public_key=public_key,
|
|
90
|
+
errors=errors):
|
|
89
91
|
# token is valid
|
|
90
92
|
bad_token = False
|
|
91
93
|
elif __IAM_LOGGER:
|
|
@@ -163,7 +165,7 @@ def service_setup_server() -> Response:
|
|
|
163
165
|
:return: *Response OK*
|
|
164
166
|
"""
|
|
165
167
|
# retrieve the request arguments
|
|
166
|
-
args: dict[str, Any] =
|
|
168
|
+
args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
|
|
167
169
|
|
|
168
170
|
# log the request
|
|
169
171
|
if __IAM_LOGGER:
|
|
@@ -209,7 +211,7 @@ def service_login() -> Response:
|
|
|
209
211
|
result: Response | None = None
|
|
210
212
|
|
|
211
213
|
# retrieve the request arguments
|
|
212
|
-
args: dict[str, Any] = dict(request.args
|
|
214
|
+
args: dict[str, Any] = dict(request.args or {})
|
|
213
215
|
|
|
214
216
|
# log the request
|
|
215
217
|
if __IAM_LOGGER:
|
|
@@ -251,7 +253,7 @@ def service_logout() -> Response:
|
|
|
251
253
|
the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
|
|
252
254
|
if the endpoint is established with a call to *iam_setup_endpoints()*.
|
|
253
255
|
|
|
254
|
-
The user is identified by the attribute *user-id* or "login", provided
|
|
256
|
+
The user is identified by the attribute *user-id* or "login", provided in the body's *JSON*.
|
|
255
257
|
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
256
258
|
Otherwise, this operation fails silently, unless an error has ocurred.
|
|
257
259
|
|
|
@@ -261,7 +263,7 @@ def service_logout() -> Response:
|
|
|
261
263
|
result: Response | None
|
|
262
264
|
|
|
263
265
|
# retrieve the request arguments
|
|
264
|
-
args: dict[str, Any] = dict(request.
|
|
266
|
+
args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
|
|
265
267
|
|
|
266
268
|
# log the request
|
|
267
269
|
if __IAM_LOGGER:
|
|
@@ -293,7 +295,7 @@ def service_logout() -> Response:
|
|
|
293
295
|
|
|
294
296
|
|
|
295
297
|
# @flask_app.route(rule=<callback_endpoint>,
|
|
296
|
-
# methods=["GET"
|
|
298
|
+
# methods=["GET"])
|
|
297
299
|
def service_callback() -> Response:
|
|
298
300
|
"""
|
|
299
301
|
Entry point for the callback from the *IAM* server on authentication operation.
|
|
@@ -319,7 +321,7 @@ def service_callback() -> Response:
|
|
|
319
321
|
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
320
322
|
"""
|
|
321
323
|
# retrieve the request arguments
|
|
322
|
-
args: dict[str, Any] = dict(request.args
|
|
324
|
+
args: dict[str, Any] = dict(request.args or {})
|
|
323
325
|
|
|
324
326
|
# log the request
|
|
325
327
|
if __IAM_LOGGER:
|
|
@@ -365,7 +367,7 @@ def service_exchange() -> Response:
|
|
|
365
367
|
If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
366
368
|
Otherwise, *errors* will contain the appropriate error message.
|
|
367
369
|
|
|
368
|
-
The expected request parameters are:
|
|
370
|
+
The expected request parameters, to be found in the body *JSON*, are:
|
|
369
371
|
- user-id: identification for the reference user (alias: 'login')
|
|
370
372
|
- access-token: the token to be exchanged
|
|
371
373
|
|
|
@@ -378,7 +380,7 @@ def service_exchange() -> Response:
|
|
|
378
380
|
:return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
379
381
|
"""
|
|
380
382
|
# retrieve the request arguments
|
|
381
|
-
args: dict[str, Any] = dict(request.
|
|
383
|
+
args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
|
|
382
384
|
|
|
383
385
|
# log the request
|
|
384
386
|
if __IAM_LOGGER:
|
|
@@ -446,7 +448,7 @@ def service_callback_exchange() -> Response:
|
|
|
446
448
|
result: Response | None = None
|
|
447
449
|
|
|
448
450
|
# retrieve the request arguments
|
|
449
|
-
args: dict[str, Any] = dict(request.args
|
|
451
|
+
args: dict[str, Any] = dict(request.args or {})
|
|
450
452
|
|
|
451
453
|
# log the request
|
|
452
454
|
if __IAM_LOGGER:
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import jwt
|
|
2
|
-
import sys
|
|
3
|
-
from jwt import PyJWK
|
|
4
|
-
from jwt.algorithms import RSAPublicKey
|
|
5
|
-
from logging import Logger
|
|
6
|
-
from pypomes_core import exc_format
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def token_get_claims(token: str,
|
|
11
|
-
errors: list[str] = None,
|
|
12
|
-
logger: Logger = None) -> dict[str, dict[str, Any]] | None:
|
|
13
|
-
"""
|
|
14
|
-
Retrieve the claims set of a JWT *token*.
|
|
15
|
-
|
|
16
|
-
Any well-constructed JWT token may be provided in *token*.
|
|
17
|
-
Note that neither the token's signature nor its expiration is verified.
|
|
18
|
-
|
|
19
|
-
:param token: the refrence token
|
|
20
|
-
:param errors: incidental error messages
|
|
21
|
-
:param logger: optional logger
|
|
22
|
-
:return: the token's claimset, or *None* if error
|
|
23
|
-
"""
|
|
24
|
-
# initialize the return variable
|
|
25
|
-
result: dict[str, dict[str, Any]] | None = None
|
|
26
|
-
|
|
27
|
-
if logger:
|
|
28
|
-
logger.debug(msg="Retrieve claims for token")
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
32
|
-
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
33
|
-
options={"verify_signature": False})
|
|
34
|
-
result = {
|
|
35
|
-
"header": header,
|
|
36
|
-
"payload": payload
|
|
37
|
-
}
|
|
38
|
-
except Exception as e:
|
|
39
|
-
exc_err: str = exc_format(exc=e,
|
|
40
|
-
exc_info=sys.exc_info())
|
|
41
|
-
if logger:
|
|
42
|
-
logger.error(msg=f"Error retrieving the token's claims: {exc_err}")
|
|
43
|
-
if isinstance(errors, list):
|
|
44
|
-
errors.append(exc_err)
|
|
45
|
-
|
|
46
|
-
return result
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def token_get_values(token: str,
|
|
50
|
-
keys: tuple[str, ...],
|
|
51
|
-
errors: list[str] = None,
|
|
52
|
-
logger: Logger = None) -> tuple:
|
|
53
|
-
"""
|
|
54
|
-
Retrieve the values of *keys* in the token's payload.
|
|
55
|
-
|
|
56
|
-
Ther values are returned in the same order as requested in *keys*.
|
|
57
|
-
For a claim not found, *None* is returned in its position.
|
|
58
|
-
|
|
59
|
-
:param token: the reference token
|
|
60
|
-
:param keys: the names of the claims whose values are to be returned
|
|
61
|
-
:param errors: incidental errors
|
|
62
|
-
:param logger: optiona logger
|
|
63
|
-
:return: a tuple containing the respective values of *claims* in *token*.
|
|
64
|
-
"""
|
|
65
|
-
token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
66
|
-
errors=errors,
|
|
67
|
-
logger=logger)
|
|
68
|
-
payload: dict[str, Any] = token_claims["payload"]
|
|
69
|
-
values: list[Any] = []
|
|
70
|
-
for key in keys:
|
|
71
|
-
values.append(payload.get(key))
|
|
72
|
-
|
|
73
|
-
return tuple(values)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def token_validate(token: str,
|
|
77
|
-
issuer: str = None,
|
|
78
|
-
recipient_id: str = None,
|
|
79
|
-
recipient_attr: str = None,
|
|
80
|
-
public_key: str | bytes | PyJWK | RSAPublicKey = None,
|
|
81
|
-
errors: list[str] = None,
|
|
82
|
-
logger: Logger = None) -> dict[str, dict[str, Any]] | None:
|
|
83
|
-
"""
|
|
84
|
-
Verify whether *token* is a valid JWT token, and return its claims (sections *header* and *payload*).
|
|
85
|
-
|
|
86
|
-
The supported public key types are:
|
|
87
|
-
- *DER*: Distinguished Encoding Rules (bytes)
|
|
88
|
-
- *PEM*: Privacy-Enhanced Mail (str)
|
|
89
|
-
- *PyJWK*: a formar from the *PyJWT* package
|
|
90
|
-
- *RSAPublicKey*: a format from the *PyJWT* package
|
|
91
|
-
|
|
92
|
-
If an asymmetric algorithm was used to sign the token and *public_key* is provided, then
|
|
93
|
-
the token is validated, by using the data in its *signature* section.
|
|
94
|
-
|
|
95
|
-
The parameters *recipient_id* and *recipient_attr* refer the token's expected subject, respectively,
|
|
96
|
-
the subject's identification and the attribute in the token's payload data identifying its subject.
|
|
97
|
-
If both are provided, *recipient_id* is validated.
|
|
98
|
-
|
|
99
|
-
On failure, *errors* will contain the reason(s) for rejecting *token*.
|
|
100
|
-
On success, return the token's claims (*header* and *payload*).
|
|
101
|
-
|
|
102
|
-
:param token: the token to be validated
|
|
103
|
-
:param public_key: optional public key used to sign the token, in *PEM* format
|
|
104
|
-
:param issuer: optional value to compare with the token's *iss* (issuer) attribute in its *payload*
|
|
105
|
-
:param recipient_id: identification of the expected token subject
|
|
106
|
-
:param recipient_attr: attribute in the token's payload holding the expected subject's identification
|
|
107
|
-
:param errors: incidental error messages
|
|
108
|
-
:param logger: optional logger
|
|
109
|
-
:return: The token's claims (*header* and *payload*), or *None* if error
|
|
110
|
-
"""
|
|
111
|
-
# initialize the return variable
|
|
112
|
-
result: dict[str, dict[str, Any]] | None = None
|
|
113
|
-
|
|
114
|
-
if logger:
|
|
115
|
-
logger.debug(msg="Validate JWT token")
|
|
116
|
-
|
|
117
|
-
# make sure to have an errors list
|
|
118
|
-
if not isinstance(errors, list):
|
|
119
|
-
errors = []
|
|
120
|
-
|
|
121
|
-
# extract needed data from token header
|
|
122
|
-
token_header: dict[str, Any] | None = None
|
|
123
|
-
try:
|
|
124
|
-
token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
125
|
-
except Exception as e:
|
|
126
|
-
exc_err: str = exc_format(exc=e,
|
|
127
|
-
exc_info=sys.exc_info())
|
|
128
|
-
if logger:
|
|
129
|
-
logger.error(msg=f"Error retrieving the token's header: {exc_err}")
|
|
130
|
-
errors.append(exc_err)
|
|
131
|
-
|
|
132
|
-
# validate the token
|
|
133
|
-
if not errors:
|
|
134
|
-
token_alg: str = token_header.get("alg")
|
|
135
|
-
require: list[str] = ["exp", "iat"]
|
|
136
|
-
if issuer:
|
|
137
|
-
require.append("iss")
|
|
138
|
-
options: dict[str, Any] = {
|
|
139
|
-
"require": require,
|
|
140
|
-
"verify_aud": False,
|
|
141
|
-
"verify_exp": True,
|
|
142
|
-
"verify_iat": True,
|
|
143
|
-
"verify_iss": issuer is not None,
|
|
144
|
-
"verify_nbf": False,
|
|
145
|
-
"verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
|
|
146
|
-
}
|
|
147
|
-
try:
|
|
148
|
-
# raises:
|
|
149
|
-
# InvalidTokenError: token is invalid
|
|
150
|
-
# InvalidKeyError: authentication key is not in the proper format
|
|
151
|
-
# ExpiredSignatureError: token and refresh period have expired
|
|
152
|
-
# InvalidSignatureError: signature does not match the one provided as part of the token
|
|
153
|
-
# ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
|
|
154
|
-
# InvalidAlgorithmError: the specified algorithm is not recognized
|
|
155
|
-
# InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
156
|
-
# MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
157
|
-
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
158
|
-
key=public_key,
|
|
159
|
-
algorithms=[token_alg],
|
|
160
|
-
options=options,
|
|
161
|
-
issuer=issuer)
|
|
162
|
-
if recipient_id and recipient_attr and \
|
|
163
|
-
payload.get(recipient_attr) and recipient_id != payload.get(recipient_attr):
|
|
164
|
-
msg: str = f"Token was issued to '{payload.get(recipient_attr)}', not to '{recipient_id}'"
|
|
165
|
-
if logger:
|
|
166
|
-
logger.error(msg=msg)
|
|
167
|
-
errors.append(msg)
|
|
168
|
-
else:
|
|
169
|
-
result = {
|
|
170
|
-
"header": token_header,
|
|
171
|
-
"payload": payload
|
|
172
|
-
}
|
|
173
|
-
except Exception as e:
|
|
174
|
-
exc_err: str = exc_format(exc=e,
|
|
175
|
-
exc_info=sys.exc_info())
|
|
176
|
-
if logger:
|
|
177
|
-
logger.error(msg=f"Error decoding the token: {exc_err}")
|
|
178
|
-
errors.append(exc_err)
|
|
179
|
-
|
|
180
|
-
if not errors and logger:
|
|
181
|
-
logger.debug(msg="Token is valid")
|
|
182
|
-
|
|
183
|
-
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|