pypomes-iam 0.2.3__py3-none-any.whl → 0.7.0__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 +20 -8
- pypomes_iam/iam_actions.py +878 -0
- pypomes_iam/iam_common.py +388 -0
- pypomes_iam/iam_pomes.py +137 -157
- pypomes_iam/iam_services.py +394 -0
- pypomes_iam/provider_pomes.py +175 -72
- pypomes_iam/token_pomes.py +63 -8
- {pypomes_iam-0.2.3.dist-info → pypomes_iam-0.7.0.dist-info}/METADATA +1 -2
- pypomes_iam-0.7.0.dist-info/RECORD +11 -0
- pypomes_iam/common_pomes.py +0 -397
- pypomes_iam/jusbr_pomes.py +0 -167
- pypomes_iam/keycloak_pomes.py +0 -170
- pypomes_iam-0.2.3.dist-info/RECORD +0 -11
- {pypomes_iam-0.2.3.dist-info → pypomes_iam-0.7.0.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.2.3.dist-info → pypomes_iam-0.7.0.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/token_pomes.py
CHANGED
|
@@ -7,8 +7,49 @@ from pypomes_core import exc_format
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
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
|
+
|
|
10
49
|
def token_validate(token: str,
|
|
11
50
|
issuer: str = None,
|
|
51
|
+
recipient_id: str = None,
|
|
52
|
+
recipient_attr: str = None,
|
|
12
53
|
public_key: str | bytes | PyJWK | RSAPublicKey = None,
|
|
13
54
|
errors: list[str] = None,
|
|
14
55
|
logger: Logger = None) -> dict[str, dict[str, Any]] | None:
|
|
@@ -24,15 +65,21 @@ def token_validate(token: str,
|
|
|
24
65
|
If an asymmetric algorithm was used to sign the token and *public_key* is provided, then
|
|
25
66
|
the token is validated, by using the data in its *signature* section.
|
|
26
67
|
|
|
68
|
+
The parameters *recipient_id* and *recipient_attr* refer the token's expected subject, respectively,
|
|
69
|
+
the subject's identification and the attribute in the token's payload data identifying its subject.
|
|
70
|
+
If both are provided, *recipient_id* is validated.
|
|
71
|
+
|
|
27
72
|
On failure, *errors* will contain the reason(s) for rejecting *token*.
|
|
28
73
|
On success, return the token's claims (*header* and *payload*).
|
|
29
74
|
|
|
30
75
|
:param token: the token to be validated
|
|
31
76
|
:param public_key: optional public key used to sign the token, in *PEM* format
|
|
32
77
|
:param issuer: optional value to compare with the token's *iss* (issuer) attribute in its *payload*
|
|
78
|
+
:param recipient_id: identification of the expected token subject
|
|
79
|
+
:param recipient_attr: attribute in the token's payload holding the expected subject's identification
|
|
33
80
|
:param errors: incidental error messages
|
|
34
81
|
:param logger: optional logger
|
|
35
|
-
:return: The token's claims (*header* and *payload*)
|
|
82
|
+
:return: The token's claims (*header* and *payload*), or *None* if error
|
|
36
83
|
"""
|
|
37
84
|
# initialize the return variable
|
|
38
85
|
result: dict[str, dict[str, Any]] | None = None
|
|
@@ -58,8 +105,11 @@ def token_validate(token: str,
|
|
|
58
105
|
# validate the token
|
|
59
106
|
if not errors:
|
|
60
107
|
token_alg: str = token_header.get("alg")
|
|
108
|
+
require: list[str] = ["exp", "iat"]
|
|
109
|
+
if issuer:
|
|
110
|
+
require.append("iss")
|
|
61
111
|
options: dict[str, Any] = {
|
|
62
|
-
"require":
|
|
112
|
+
"require": require,
|
|
63
113
|
"verify_aud": False,
|
|
64
114
|
"verify_exp": True,
|
|
65
115
|
"verify_iat": True,
|
|
@@ -67,8 +117,6 @@ def token_validate(token: str,
|
|
|
67
117
|
"verify_nbf": False,
|
|
68
118
|
"verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
|
|
69
119
|
}
|
|
70
|
-
if issuer:
|
|
71
|
-
options["require"].append("iss")
|
|
72
120
|
try:
|
|
73
121
|
# raises:
|
|
74
122
|
# InvalidTokenError: token is invalid
|
|
@@ -84,10 +132,17 @@ def token_validate(token: str,
|
|
|
84
132
|
algorithms=[token_alg],
|
|
85
133
|
options=options,
|
|
86
134
|
issuer=issuer)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"payload"
|
|
90
|
-
|
|
135
|
+
if recipient_id and recipient_attr and \
|
|
136
|
+
payload.get(recipient_attr) and recipient_id != payload.get(recipient_attr):
|
|
137
|
+
msg: str = f"Token was issued to '{payload.get(recipient_attr)}', not to '{recipient_id}'"
|
|
138
|
+
if logger:
|
|
139
|
+
logger.error(msg=msg)
|
|
140
|
+
errors.append(msg)
|
|
141
|
+
else:
|
|
142
|
+
result = {
|
|
143
|
+
"header": token_header,
|
|
144
|
+
"payload": payload
|
|
145
|
+
}
|
|
91
146
|
except Exception as e:
|
|
92
147
|
exc_err: str = exc_format(exc=e,
|
|
93
148
|
exc_info=sys.exc_info())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -10,7 +10,6 @@ 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: cachetools>=6.2.1
|
|
14
13
|
Requires-Dist: flask>=3.1.2
|
|
15
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
16
15
|
Requires-Dist: pypomes-core>=2.8.1
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
|
|
2
|
+
pypomes_iam/iam_actions.py,sha256=5nomjeylTUSEtLCAvRnM1ayblsVx2hGDYzQn2twk8kk,42727
|
|
3
|
+
pypomes_iam/iam_common.py,sha256=ki_-m6fqJqUbGjgTD41r9zaE-FOXgA_c_tLisIYYTfU,15457
|
|
4
|
+
pypomes_iam/iam_pomes.py,sha256=_kLnrZG25XhJsIv3wqDl_2sIJ2ho_2TIMKrPCyPmA7Q,7362
|
|
5
|
+
pypomes_iam/iam_services.py,sha256=uUD333SaTbo8MGRyIp5GGil7HAupK73ym4_bKtGkPFg,15878
|
|
6
|
+
pypomes_iam/provider_pomes.py,sha256=3mMj5LQs53YEINUEOfFBAxOwOP3aOR_szlE4daEBLK0,10523
|
|
7
|
+
pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
|
|
8
|
+
pypomes_iam-0.7.0.dist-info/METADATA,sha256=H2XjOEqG8t1umbwLIj8CM4AU0G9ufNN0mbEPIHfH4ko,661
|
|
9
|
+
pypomes_iam-0.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pypomes_iam-0.7.0.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
+
pypomes_iam-0.7.0.dist-info/RECORD,,
|
pypomes_iam/common_pomes.py
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import requests
|
|
3
|
-
import secrets
|
|
4
|
-
import string
|
|
5
|
-
import sys
|
|
6
|
-
from cachetools import Cache
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from flask import Request
|
|
9
|
-
from logging import Logger
|
|
10
|
-
from pypomes_core import TZ_LOCAL, exc_format
|
|
11
|
-
from pypomes_crypto import crypto_jwk_convert
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
# registry structure:
|
|
15
|
-
# {
|
|
16
|
-
# "client-id": <str>,
|
|
17
|
-
# "client-secret": <str>,
|
|
18
|
-
# "client-timeout": <int>,
|
|
19
|
-
# "public_key": <str>,
|
|
20
|
-
# "key-lifetime": <int>,
|
|
21
|
-
# "key-expiration": <int>,
|
|
22
|
-
# "base-url": <str>,
|
|
23
|
-
# "callback-url": <str>,
|
|
24
|
-
# "safe-cache": <FIFOCache>
|
|
25
|
-
# }
|
|
26
|
-
# data in "safe-cache":
|
|
27
|
-
# {
|
|
28
|
-
# "users": {
|
|
29
|
-
# "<user-id>": {
|
|
30
|
-
# "access-token": <str>
|
|
31
|
-
# "refresh-token": <str>
|
|
32
|
-
# "access-expiration": <timestamp>,
|
|
33
|
-
# "login-expiration": <timestamp>, <-- transient
|
|
34
|
-
# "login-id": <str>, <-- transient
|
|
35
|
-
# }
|
|
36
|
-
# }
|
|
37
|
-
# }
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _service_login(registry: dict[str, Any],
|
|
41
|
-
args: dict[str, Any],
|
|
42
|
-
logger: Logger | None) -> str:
|
|
43
|
-
"""
|
|
44
|
-
Build the callback URL for redirecting the request to the IAM's authentication page.
|
|
45
|
-
|
|
46
|
-
:param registry: the registry holding the authentication data
|
|
47
|
-
:param args: the arguments passed when requesting the service
|
|
48
|
-
:param logger: optional logger
|
|
49
|
-
:return: the callback URL, with the appropriate parameters
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
# retrieve user data
|
|
53
|
-
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
54
|
-
|
|
55
|
-
# build the user data
|
|
56
|
-
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
57
|
-
user_data: dict[str, Any] = _get_user_data(registry=registry,
|
|
58
|
-
user_id=oauth_state,
|
|
59
|
-
logger=logger)
|
|
60
|
-
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
61
|
-
user_data["login-id"] = user_id
|
|
62
|
-
timeout: int = _get_login_timeout(registry=registry)
|
|
63
|
-
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
|
|
64
|
-
|
|
65
|
-
# build the redirect url
|
|
66
|
-
result: str = (f"{registry["base-url"]}/protocol/openid-connect/auth"
|
|
67
|
-
f"?response_type=code&scope=openid"
|
|
68
|
-
f"&client_id={registry["client-id"]}"
|
|
69
|
-
f"&redirect_uri={registry["callback-url"]}"
|
|
70
|
-
f"&state={oauth_state}")
|
|
71
|
-
|
|
72
|
-
# logout the user
|
|
73
|
-
_service_logout(registry=registry,
|
|
74
|
-
args=args,
|
|
75
|
-
logger=logger)
|
|
76
|
-
return result
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _service_logout(registry: dict[str, Any],
|
|
80
|
-
args: dict[str, Any],
|
|
81
|
-
logger: Logger | None) -> None:
|
|
82
|
-
"""
|
|
83
|
-
Remove all data associating *user_id* from *registry*.
|
|
84
|
-
|
|
85
|
-
:param registry: the registry holding the authentication data
|
|
86
|
-
:param args: the arguments passed when requesting the service
|
|
87
|
-
:param logger: optional logger
|
|
88
|
-
"""
|
|
89
|
-
# remove the user data
|
|
90
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
91
|
-
if user_id:
|
|
92
|
-
cache: Cache = registry["safe-cache"]
|
|
93
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
94
|
-
if user_id in users:
|
|
95
|
-
users.pop(user_id)
|
|
96
|
-
if logger:
|
|
97
|
-
logger.debug(msg=f"User '{user_id}' removed from the registry")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _service_callback(registry: dict[str, Any],
|
|
101
|
-
args: dict[str, Any],
|
|
102
|
-
errors: list[str],
|
|
103
|
-
logger: Logger | None) -> tuple[str, str]:
|
|
104
|
-
"""
|
|
105
|
-
Entry point for the callback from JusBR on authentication operation.
|
|
106
|
-
|
|
107
|
-
:param registry: the registry holding the authentication data
|
|
108
|
-
:param args: the arguments passed when requesting the service
|
|
109
|
-
:param errors: incidental errors
|
|
110
|
-
:param logger: optional logger
|
|
111
|
-
"""
|
|
112
|
-
from .token_pomes import token_validate
|
|
113
|
-
|
|
114
|
-
# initialize the return variable
|
|
115
|
-
result: tuple[str, str] | None = None
|
|
116
|
-
|
|
117
|
-
# retrieve the users authentication data
|
|
118
|
-
cache: Cache = registry["safe-cache"]
|
|
119
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
120
|
-
|
|
121
|
-
# validate the OAuth2 state
|
|
122
|
-
oauth_state: str = args.get("state")
|
|
123
|
-
user_data: dict[str, Any] | None = None
|
|
124
|
-
if oauth_state:
|
|
125
|
-
for user, data in users.items():
|
|
126
|
-
if user == oauth_state:
|
|
127
|
-
user_data = data
|
|
128
|
-
break
|
|
129
|
-
|
|
130
|
-
# exchange 'code' for the token
|
|
131
|
-
if user_data:
|
|
132
|
-
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
133
|
-
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
134
|
-
errors.append("Operation timeout")
|
|
135
|
-
else:
|
|
136
|
-
users.pop(oauth_state)
|
|
137
|
-
code: str = args.get("code")
|
|
138
|
-
body_data: dict[str, Any] = {
|
|
139
|
-
"grant_type": "authorization_code",
|
|
140
|
-
"code": code,
|
|
141
|
-
"redirect_uri": registry.get("callback-url"),
|
|
142
|
-
}
|
|
143
|
-
token = _post_for_token(registry=registry,
|
|
144
|
-
user_data=user_data,
|
|
145
|
-
body_data=body_data,
|
|
146
|
-
errors=errors,
|
|
147
|
-
logger=logger)
|
|
148
|
-
# retrieve the token's claims
|
|
149
|
-
if not errors:
|
|
150
|
-
public_key: bytes = _get_public_key(registry=registry,
|
|
151
|
-
logger=logger)
|
|
152
|
-
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
153
|
-
issuer=registry["base-url"],
|
|
154
|
-
public_key=public_key,
|
|
155
|
-
errors=errors,
|
|
156
|
-
logger=logger)
|
|
157
|
-
if not errors:
|
|
158
|
-
token_user: str = token_claims["payload"].get("preferred_username")
|
|
159
|
-
if token_user == oauth_state:
|
|
160
|
-
users[token_user] = user_data
|
|
161
|
-
result = (token_user, token)
|
|
162
|
-
else:
|
|
163
|
-
errors.append(f"Token was issued to user '{token_user}'")
|
|
164
|
-
else:
|
|
165
|
-
errors.append("Unknown state received")
|
|
166
|
-
|
|
167
|
-
return result
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _service_token(registry: dict[str, Any],
|
|
171
|
-
args: dict[str, Any],
|
|
172
|
-
errors: list[str] = None,
|
|
173
|
-
logger: Logger = None) -> str:
|
|
174
|
-
"""
|
|
175
|
-
Retrieve the authentication token for user *user_id*.
|
|
176
|
-
|
|
177
|
-
:param registry: the registry holding the authentication data
|
|
178
|
-
:param args: the arguments passed when requesting the service
|
|
179
|
-
:param errors: incidental error messages
|
|
180
|
-
:param logger: optional logger
|
|
181
|
-
:return: the token for *user_id*, or *None* if error
|
|
182
|
-
"""
|
|
183
|
-
# initialize the return variable
|
|
184
|
-
result: str | None = None
|
|
185
|
-
|
|
186
|
-
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
187
|
-
user_data: dict[str, Any] = _get_user_data(registry=registry,
|
|
188
|
-
user_id=user_id,
|
|
189
|
-
logger=logger)
|
|
190
|
-
token: str = user_data["access-token"]
|
|
191
|
-
if token:
|
|
192
|
-
access_expiration: int = user_data.get("access-expiration")
|
|
193
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
194
|
-
if now < access_expiration:
|
|
195
|
-
result = token
|
|
196
|
-
else:
|
|
197
|
-
# access token has expired
|
|
198
|
-
refresh_token: str = user_data["refresh-token"]
|
|
199
|
-
if refresh_token:
|
|
200
|
-
body_data: dict[str, str] = {
|
|
201
|
-
"grant_type": "refresh_token",
|
|
202
|
-
"refresh_token": refresh_token
|
|
203
|
-
}
|
|
204
|
-
result = _post_for_token(registry=registry,
|
|
205
|
-
user_data=user_data,
|
|
206
|
-
body_data=body_data,
|
|
207
|
-
errors=errors,
|
|
208
|
-
logger=logger)
|
|
209
|
-
|
|
210
|
-
elif logger or isinstance(errors, list):
|
|
211
|
-
err_msg: str = f"User '{user_id}' not authenticated"
|
|
212
|
-
if isinstance(errors, list):
|
|
213
|
-
errors.append(err_msg)
|
|
214
|
-
if logger:
|
|
215
|
-
logger.error(msg=err_msg)
|
|
216
|
-
|
|
217
|
-
return result
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _get_public_key(registry: dict[str, Any],
|
|
221
|
-
logger: Logger | None) -> bytes:
|
|
222
|
-
"""
|
|
223
|
-
Obtain the public key used by the *IAM* to sign the authentication tokens.
|
|
224
|
-
|
|
225
|
-
The public key is saved in *registry*.
|
|
226
|
-
|
|
227
|
-
:param registry: the registry holding the authentication data
|
|
228
|
-
:return: the public key, in *DER* format
|
|
229
|
-
"""
|
|
230
|
-
# initialize the return variable
|
|
231
|
-
result: bytes | None = None
|
|
232
|
-
|
|
233
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
234
|
-
if now > registry["key-expiration"]:
|
|
235
|
-
# obtain a new public key
|
|
236
|
-
url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
|
|
237
|
-
if logger:
|
|
238
|
-
logger.debug(msg=f"GET '{url}'")
|
|
239
|
-
response: requests.Response = requests.get(url=url)
|
|
240
|
-
if response.status_code == 200:
|
|
241
|
-
# request succeeded
|
|
242
|
-
if logger:
|
|
243
|
-
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
244
|
-
reply: dict[str, Any] = response.json()
|
|
245
|
-
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
246
|
-
fmt="DER")
|
|
247
|
-
registry["public-key"] = result
|
|
248
|
-
duration: int = registry["key-lifetime"] or 0
|
|
249
|
-
registry["key-expiration"] = now + duration
|
|
250
|
-
elif logger:
|
|
251
|
-
msg: str = f"GET failure, status {response.status_code}, reason '{response.reason}'"
|
|
252
|
-
if hasattr(response, "content") and response.content:
|
|
253
|
-
msg += f", content '{response.content}'"
|
|
254
|
-
logger.error(msg=msg)
|
|
255
|
-
else:
|
|
256
|
-
result = registry["public-key"]
|
|
257
|
-
|
|
258
|
-
return result
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def _get_login_timeout(registry: dict[str, Any]) -> int | None:
|
|
262
|
-
"""
|
|
263
|
-
Retrieve from *registry* the timeout currently applicable for the login operation.
|
|
264
|
-
|
|
265
|
-
:param registry: the registry holding the authentication data
|
|
266
|
-
:return: the current login timeout, or *None* if none has been set.
|
|
267
|
-
"""
|
|
268
|
-
timeout: int = registry.get("client-timeout")
|
|
269
|
-
return timeout if isinstance(timeout, int) and timeout > 0 else None
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
def _get_user_data(registry: dict[str, Any],
|
|
273
|
-
user_id: str,
|
|
274
|
-
logger: Logger | None) -> dict[str, Any]:
|
|
275
|
-
"""
|
|
276
|
-
Retrieve the data for *user_id* from *registry*.
|
|
277
|
-
|
|
278
|
-
If an entry is not found for *user_id* in the registry, it is created.
|
|
279
|
-
It will remain there until the user is logged out.
|
|
280
|
-
|
|
281
|
-
:param registry: the registry holding the authentication data
|
|
282
|
-
:return: the data for *user_id* in the registry
|
|
283
|
-
"""
|
|
284
|
-
cache: Cache = registry["safe-cache"]
|
|
285
|
-
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
286
|
-
result: dict[str, Any] = users.get(user_id)
|
|
287
|
-
if not result:
|
|
288
|
-
result = {
|
|
289
|
-
"access-token": None,
|
|
290
|
-
"refresh-token": None,
|
|
291
|
-
"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
292
|
-
}
|
|
293
|
-
users[user_id] = result
|
|
294
|
-
if logger:
|
|
295
|
-
logger.debug(msg=f"Entry for user '{user_id}' added to the registry")
|
|
296
|
-
elif logger:
|
|
297
|
-
logger.debug(msg=f"Entry for user '{user_id}' obtained from the registry")
|
|
298
|
-
|
|
299
|
-
return result
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def _post_for_token(registry: dict[str, Any],
|
|
303
|
-
user_data: dict[str, Any],
|
|
304
|
-
body_data: dict[str, Any],
|
|
305
|
-
errors: list[str] | None,
|
|
306
|
-
logger: Logger | None) -> str | None:
|
|
307
|
-
"""
|
|
308
|
-
Send a POST request to obtain the authentication token data, and return the access token.
|
|
309
|
-
|
|
310
|
-
For token exchange, *body_data* will have the attributes
|
|
311
|
-
- "grant_type": "authorization_code"
|
|
312
|
-
- "code": <16-character-random-code>
|
|
313
|
-
- "redirect_uri": <callback-url>
|
|
314
|
-
For token refresh, *body_data* will have the attributes
|
|
315
|
-
- "grant_type": "refresh_token"
|
|
316
|
-
- "refresh_token": <current-refresh-token>
|
|
317
|
-
|
|
318
|
-
If the operation is successful, the token data is stored in the registry.
|
|
319
|
-
Otherwise, *errors* will contain the appropriate error message.
|
|
320
|
-
|
|
321
|
-
:param registry: the registry holding the authentication data
|
|
322
|
-
:param user_data: the user's data in the registry
|
|
323
|
-
:param body_data: the data to send in the body of the request
|
|
324
|
-
:param errors: incidental errors
|
|
325
|
-
:param logger: optional logger
|
|
326
|
-
:return: the access token obtained, or *None* if error
|
|
327
|
-
"""
|
|
328
|
-
# initialize the return variable
|
|
329
|
-
result: str | None = None
|
|
330
|
-
|
|
331
|
-
# complete the data to send in body of request
|
|
332
|
-
body_data["client_id"] = registry["client-id"]
|
|
333
|
-
client_secret: str = registry["client-secret"]
|
|
334
|
-
if client_secret:
|
|
335
|
-
body_data["client_secret"] = client_secret
|
|
336
|
-
|
|
337
|
-
# obtain the token
|
|
338
|
-
err_msg: str | None = None
|
|
339
|
-
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
340
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
341
|
-
if logger:
|
|
342
|
-
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
343
|
-
ensure_ascii=False)}")
|
|
344
|
-
try:
|
|
345
|
-
# typical return on a token request:
|
|
346
|
-
# {
|
|
347
|
-
# "token_type": "Bearer",
|
|
348
|
-
# "access_token": <str>,
|
|
349
|
-
# "expires_in": <number-of-seconds>,
|
|
350
|
-
# "refresh_token": <str>
|
|
351
|
-
# }
|
|
352
|
-
response: requests.Response = requests.post(url=url,
|
|
353
|
-
data=body_data)
|
|
354
|
-
if response.status_code == 200:
|
|
355
|
-
# request succeeded
|
|
356
|
-
if logger:
|
|
357
|
-
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
358
|
-
reply: dict[str, Any] = response.json()
|
|
359
|
-
result = reply.get("access_token")
|
|
360
|
-
user_data["access-token"] = result
|
|
361
|
-
# on token refresh, keep current refresh token if a new one is not provided
|
|
362
|
-
user_data["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
|
|
363
|
-
user_data["access-expiration"] = now + reply.get("expires_in")
|
|
364
|
-
else:
|
|
365
|
-
# request resulted in error
|
|
366
|
-
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
367
|
-
if hasattr(response, "content") and response.content:
|
|
368
|
-
err_msg += f", content '{response.content}'"
|
|
369
|
-
if response.status_code == 400 and body_data.get("grant_type") == "refresh_token":
|
|
370
|
-
# refresh token is no longer valid
|
|
371
|
-
user_data["refresh-token"] = None
|
|
372
|
-
except Exception as e:
|
|
373
|
-
# the operation raised an exception
|
|
374
|
-
err_msg = exc_format(exc=e,
|
|
375
|
-
exc_info=sys.exc_info())
|
|
376
|
-
err_msg = f"POST '{url}': error '{err_msg}'"
|
|
377
|
-
|
|
378
|
-
if err_msg:
|
|
379
|
-
if isinstance(errors, list):
|
|
380
|
-
errors.append(err_msg)
|
|
381
|
-
if logger:
|
|
382
|
-
logger.error(msg=err_msg)
|
|
383
|
-
|
|
384
|
-
return result
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def _log_init(request: Request) -> str:
|
|
388
|
-
"""
|
|
389
|
-
Build the messages for logging the request entry.
|
|
390
|
-
|
|
391
|
-
:param request: the Request object
|
|
392
|
-
:return: the log message
|
|
393
|
-
"""
|
|
394
|
-
|
|
395
|
-
params: str = json.dumps(obj=request.args,
|
|
396
|
-
ensure_ascii=False)
|
|
397
|
-
return f"Request {request.method}:{request.path}, params {params}"
|