pypomes-iam 0.5.1__py3-none-any.whl → 0.6.9__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.
- pypomes_iam/__init__.py +16 -18
- pypomes_iam/iam_actions.py +870 -0
- pypomes_iam/iam_common.py +217 -83
- pypomes_iam/iam_pomes.py +131 -486
- pypomes_iam/iam_services.py +138 -53
- pypomes_iam/provider_pomes.py +160 -79
- pypomes_iam/token_pomes.py +39 -2
- {pypomes_iam-0.5.1.dist-info → pypomes_iam-0.6.9.dist-info}/METADATA +1 -2
- pypomes_iam-0.6.9.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -122
- pypomes_iam/keycloak_pomes.py +0 -136
- pypomes_iam-0.5.1.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.1.dist-info → pypomes_iam-0.6.9.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.1.dist-info → pypomes_iam-0.6.9.dist-info}/licenses/LICENSE +0 -0
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
from .iam_common import (
|
|
7
|
+
IamServer, IamParam, _iam_lock,
|
|
8
|
+
_get_iam_registry, _get_public_key,
|
|
9
|
+
_iam_server_from_endpoint, _iam_server_from_issuer
|
|
10
10
|
)
|
|
11
|
+
from .iam_actions import (
|
|
12
|
+
action_login, action_logout,
|
|
13
|
+
action_token, action_exchange, action_callback
|
|
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[IamParam.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.
|
|
@@ -55,20 +140,20 @@ def service_login() -> Response:
|
|
|
55
140
|
errors: list[str] = []
|
|
56
141
|
with _iam_lock:
|
|
57
142
|
# retrieve the IAM server
|
|
58
|
-
iam_server: IamServer =
|
|
59
|
-
|
|
60
|
-
|
|
143
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
144
|
+
errors=errors,
|
|
145
|
+
logger=__IAM_LOGGER)
|
|
61
146
|
if iam_server:
|
|
62
147
|
# obtain the login URL
|
|
63
|
-
login_url: str =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
148
|
+
login_url: str = action_login(iam_server=iam_server,
|
|
149
|
+
args=request.args,
|
|
150
|
+
errors=errors,
|
|
151
|
+
logger=__IAM_LOGGER)
|
|
67
152
|
if login_url:
|
|
68
153
|
result = jsonify({"login-url": login_url})
|
|
69
154
|
if errors:
|
|
70
|
-
result = Response("; ".join(errors)
|
|
71
|
-
|
|
155
|
+
result = Response(response="; ".join(errors),
|
|
156
|
+
status=400)
|
|
72
157
|
|
|
73
158
|
# log the response
|
|
74
159
|
if __IAM_LOGGER:
|
|
@@ -83,7 +168,7 @@ def service_login() -> Response:
|
|
|
83
168
|
# methods=["GET"])
|
|
84
169
|
def service_logout() -> Response:
|
|
85
170
|
"""
|
|
86
|
-
Entry point for the
|
|
171
|
+
Entry point for the IAM server's logout service.
|
|
87
172
|
|
|
88
173
|
The user is identified by the attribute *user-id* or "login", provided as a request parameter.
|
|
89
174
|
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
@@ -101,18 +186,18 @@ def service_logout() -> Response:
|
|
|
101
186
|
errors: list[str] = []
|
|
102
187
|
with _iam_lock:
|
|
103
188
|
# retrieve the IAM server
|
|
104
|
-
iam_server: IamServer =
|
|
105
|
-
|
|
106
|
-
|
|
189
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
190
|
+
errors=errors,
|
|
191
|
+
logger=__IAM_LOGGER)
|
|
107
192
|
if iam_server:
|
|
108
193
|
# logout the user
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
194
|
+
action_logout(iam_server=iam_server,
|
|
195
|
+
args=request.args,
|
|
196
|
+
errors=errors,
|
|
197
|
+
logger=__IAM_LOGGER)
|
|
113
198
|
if errors:
|
|
114
|
-
result = Response("; ".join(errors)
|
|
115
|
-
|
|
199
|
+
result = Response(response="; ".join(errors),
|
|
200
|
+
status=400)
|
|
116
201
|
else:
|
|
117
202
|
result = Response(status=204)
|
|
118
203
|
|
|
@@ -129,7 +214,7 @@ def service_logout() -> Response:
|
|
|
129
214
|
# methods=["POST"])
|
|
130
215
|
def service_callback() -> Response:
|
|
131
216
|
"""
|
|
132
|
-
Entry point for the callback from
|
|
217
|
+
Entry point for the callback from the IAM server on authentication operation.
|
|
133
218
|
|
|
134
219
|
This callback is invoked from a front-end application after a successful login at the
|
|
135
220
|
*IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
|
|
@@ -155,15 +240,15 @@ def service_callback() -> Response:
|
|
|
155
240
|
token_data: tuple[str, str] | None = None
|
|
156
241
|
with _iam_lock:
|
|
157
242
|
# retrieve the IAM server
|
|
158
|
-
iam_server: IamServer =
|
|
159
|
-
|
|
160
|
-
|
|
243
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
244
|
+
errors=errors,
|
|
245
|
+
logger=__IAM_LOGGER)
|
|
161
246
|
if iam_server:
|
|
162
247
|
# process the callback operation
|
|
163
|
-
token_data =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
248
|
+
token_data = action_callback(iam_server=iam_server,
|
|
249
|
+
args=request.args,
|
|
250
|
+
errors=errors,
|
|
251
|
+
logger=__IAM_LOGGER)
|
|
167
252
|
result: Response
|
|
168
253
|
if errors:
|
|
169
254
|
result = jsonify({"errors": "; ".join(errors)})
|
|
@@ -171,9 +256,9 @@ def service_callback() -> Response:
|
|
|
171
256
|
else:
|
|
172
257
|
result = jsonify({"user-id": token_data[0],
|
|
173
258
|
"access-token": token_data[1]})
|
|
174
|
-
# log the response
|
|
175
259
|
if __IAM_LOGGER:
|
|
176
|
-
|
|
260
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
261
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
177
262
|
|
|
178
263
|
return result
|
|
179
264
|
|
|
@@ -209,16 +294,16 @@ def service_token() -> Response:
|
|
|
209
294
|
if user_id:
|
|
210
295
|
with _iam_lock:
|
|
211
296
|
# retrieve the IAM server
|
|
212
|
-
iam_server: IamServer =
|
|
213
|
-
|
|
214
|
-
|
|
297
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
298
|
+
errors=errors,
|
|
299
|
+
logger=__IAM_LOGGER)
|
|
215
300
|
if iam_server:
|
|
216
301
|
# retrieve the token
|
|
217
302
|
errors: list[str] = []
|
|
218
|
-
token: str =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
303
|
+
token: str = action_token(iam_server=iam_server,
|
|
304
|
+
args=args,
|
|
305
|
+
errors=errors,
|
|
306
|
+
logger=__IAM_LOGGER)
|
|
222
307
|
else:
|
|
223
308
|
msg: str = "User identification not provided"
|
|
224
309
|
errors.append(msg)
|
|
@@ -227,14 +312,14 @@ def service_token() -> Response:
|
|
|
227
312
|
|
|
228
313
|
result: Response
|
|
229
314
|
if errors:
|
|
230
|
-
result = Response("; ".join(errors)
|
|
231
|
-
|
|
315
|
+
result = Response(response="; ".join(errors),
|
|
316
|
+
status=400)
|
|
232
317
|
else:
|
|
233
318
|
result = jsonify({"user-id": user_id,
|
|
234
319
|
"access-token": token})
|
|
235
|
-
# log the response
|
|
236
320
|
if __IAM_LOGGER:
|
|
237
|
-
|
|
321
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
322
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
238
323
|
|
|
239
324
|
return result
|
|
240
325
|
|
|
@@ -271,21 +356,21 @@ def service_exchange() -> Response:
|
|
|
271
356
|
errors: list[str] = []
|
|
272
357
|
with _iam_lock:
|
|
273
358
|
# retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
|
|
274
|
-
iam_server: IamServer =
|
|
275
|
-
|
|
276
|
-
|
|
359
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
360
|
+
errors=errors,
|
|
361
|
+
logger=__IAM_LOGGER)
|
|
277
362
|
# exchange the token
|
|
278
363
|
token_data: dict[str, Any] | None = None
|
|
279
364
|
if iam_server:
|
|
280
365
|
errors: list[str] = []
|
|
281
|
-
token_data =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
366
|
+
token_data = action_exchange(iam_server=iam_server,
|
|
367
|
+
args=request.args,
|
|
368
|
+
errors=errors,
|
|
369
|
+
logger=__IAM_LOGGER)
|
|
285
370
|
result: Response
|
|
286
371
|
if errors:
|
|
287
|
-
result = Response("; ".join(errors)
|
|
288
|
-
|
|
372
|
+
result = Response(response="; ".join(errors),
|
|
373
|
+
status=400)
|
|
289
374
|
else:
|
|
290
375
|
result = jsonify(token_data)
|
|
291
376
|
|
pypomes_iam/provider_pomes.py
CHANGED
|
@@ -3,29 +3,49 @@ import requests
|
|
|
3
3
|
import sys
|
|
4
4
|
from base64 import b64encode
|
|
5
5
|
from datetime import datetime
|
|
6
|
+
from enum import StrEnum
|
|
6
7
|
from logging import Logger
|
|
7
8
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
8
|
-
from threading import
|
|
9
|
+
from threading import Lock
|
|
9
10
|
from typing import Any, Final
|
|
10
11
|
|
|
12
|
+
|
|
13
|
+
class ProviderParam(StrEnum):
|
|
14
|
+
"""
|
|
15
|
+
Parameters for configuring a *JWT* token provider.
|
|
16
|
+
"""
|
|
17
|
+
URL = "url"
|
|
18
|
+
USER = "user"
|
|
19
|
+
PWD = "pwd"
|
|
20
|
+
CUSTOM_AUTH = "custom-auth"
|
|
21
|
+
HEADER_DATA = "headers-data"
|
|
22
|
+
BODY_DATA = "body-data"
|
|
23
|
+
ACCESS_TOKEN = "access-token"
|
|
24
|
+
ACCESS_EXPIRATION = "access-expiration"
|
|
25
|
+
REFRESH_TOKEN = "refresh-token"
|
|
26
|
+
REFRESH_EXPIRATION = "refresh-expiration"
|
|
27
|
+
|
|
28
|
+
|
|
11
29
|
# structure:
|
|
12
30
|
# {
|
|
13
31
|
# <provider-id>: {
|
|
14
32
|
# "url": <strl>,
|
|
15
33
|
# "user": <str>,
|
|
16
34
|
# "pwd": <str>,
|
|
17
|
-
# "
|
|
35
|
+
# "custom-auth": <bool>,
|
|
18
36
|
# "headers-data": <dict[str, str]>,
|
|
19
37
|
# "body-data": <dict[str, str],
|
|
20
38
|
# "access-token": <str>,
|
|
21
|
-
# "access-expiration": <timestamp
|
|
39
|
+
# "access-expiration": <timestamp>,
|
|
40
|
+
# "refresh-token": <str>,
|
|
41
|
+
# "refresh-expiration": <timestamp>
|
|
22
42
|
# }
|
|
23
43
|
# }
|
|
24
44
|
_provider_registry: Final[dict[str, dict[str, Any]]] = {}
|
|
25
45
|
|
|
26
46
|
# the lock protecting the data in '_provider_registry'
|
|
27
47
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
28
|
-
_provider_lock: Final[
|
|
48
|
+
_provider_lock: Final[Lock] = Lock()
|
|
29
49
|
|
|
30
50
|
|
|
31
51
|
def provider_register(provider_id: str,
|
|
@@ -58,16 +78,16 @@ def provider_register(provider_id: str,
|
|
|
58
78
|
|
|
59
79
|
with _provider_lock:
|
|
60
80
|
_provider_registry[provider_id] = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
ProviderParam.URL: auth_url,
|
|
82
|
+
ProviderParam.USER: auth_user,
|
|
83
|
+
ProviderParam.PWD: auth_pwd,
|
|
84
|
+
ProviderParam.CUSTOM_AUTH: custom_auth,
|
|
85
|
+
ProviderParam.HEADER_DATA: headers_data,
|
|
86
|
+
ProviderParam.BODY_DATA: body_data,
|
|
87
|
+
ProviderParam.ACCESS_TOKEN: None,
|
|
88
|
+
ProviderParam.ACCESS_EXPIRATION: 0,
|
|
89
|
+
ProviderParam.REFRESH_TOKEN: None,
|
|
90
|
+
ProviderParam.REFRESH_EXPIRATION: 0
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
|
|
@@ -86,76 +106,137 @@ def provider_get_token(provider_id: str,
|
|
|
86
106
|
# initialize the return variable
|
|
87
107
|
result: str | None = None
|
|
88
108
|
|
|
89
|
-
err_msg: str | None = None
|
|
90
109
|
with _provider_lock:
|
|
91
110
|
provider: dict[str, Any] = _provider_registry.get(provider_id)
|
|
92
111
|
if provider:
|
|
93
|
-
now:
|
|
94
|
-
if now
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# request resulted in error, report the problem
|
|
126
|
-
err_msg = (f"POST failure, "
|
|
127
|
-
f"status {response.status_code}, reason {response.reason}")
|
|
112
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
113
|
+
if now < provider.get(ProviderParam.ACCESS_EXPIRATION):
|
|
114
|
+
# retrieve the stored access token
|
|
115
|
+
result = provider.get(ProviderParam.ACCESS_TOKEN)
|
|
116
|
+
else:
|
|
117
|
+
# access token has expired
|
|
118
|
+
header_data: dict[str, str] | None = None
|
|
119
|
+
body_data: dict[str, str] | None = None
|
|
120
|
+
url: str = provider.get(ProviderParam.URL)
|
|
121
|
+
refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
|
|
122
|
+
if refresh_token:
|
|
123
|
+
# refresh token exists
|
|
124
|
+
refresh_expiration: int = provider.get(ProviderParam.REFRESH_EXPIRATION)
|
|
125
|
+
if now < refresh_expiration:
|
|
126
|
+
# refresh token has not expired
|
|
127
|
+
header_data: dict[str, str] = {
|
|
128
|
+
"Content-Type": "application/json"
|
|
129
|
+
}
|
|
130
|
+
body_data: dict[str, str] = {
|
|
131
|
+
"grant_type": "refresh_token",
|
|
132
|
+
"refresh_token": refresh_token
|
|
133
|
+
}
|
|
134
|
+
if not body_data:
|
|
135
|
+
# refresh token does not exist or has expired
|
|
136
|
+
user: str = provider.get(ProviderParam.USER)
|
|
137
|
+
pwd: str = provider.get(ProviderParam.PWD)
|
|
138
|
+
headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
|
|
139
|
+
body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
|
|
140
|
+
custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
|
|
141
|
+
if custom_auth:
|
|
142
|
+
body_data[custom_auth[0]] = user
|
|
143
|
+
body_data[custom_auth[1]] = pwd
|
|
128
144
|
else:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
errors
|
|
154
|
-
|
|
155
|
-
logger.error(msg=err_msg)
|
|
156
|
-
else:
|
|
157
|
-
result = provider.get("access-token")
|
|
145
|
+
enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
|
|
146
|
+
headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
|
|
147
|
+
|
|
148
|
+
# obtain the token
|
|
149
|
+
token_data: dict[str, Any] = __post_for_token(url=url,
|
|
150
|
+
header_data=header_data,
|
|
151
|
+
body_data=body_data,
|
|
152
|
+
errors=errors,
|
|
153
|
+
logger=logger)
|
|
154
|
+
if token_data:
|
|
155
|
+
result = token_data.get("access_token")
|
|
156
|
+
provider[ProviderParam.ACCESS_TOKEN] = result
|
|
157
|
+
provider[ProviderParam.ACCESS_EXPIRATION] = now + token_data.get("expires_in")
|
|
158
|
+
refresh_token = token_data.get("refresh_token")
|
|
159
|
+
if refresh_token:
|
|
160
|
+
provider[ProviderParam.REFRESH_TOKEN] = refresh_token
|
|
161
|
+
refresh_exp: int = token_data.get("refresh_expires_in")
|
|
162
|
+
provider[ProviderParam.REFRESH_EXPIRATION] = (now + refresh_exp) \
|
|
163
|
+
if refresh_exp else sys.maxsize
|
|
164
|
+
|
|
165
|
+
elif logger or isinstance(errors, list):
|
|
166
|
+
msg: str = f"Unknown provider '{provider_id}'"
|
|
167
|
+
if logger:
|
|
168
|
+
logger.error(msg=msg)
|
|
169
|
+
if isinstance(errors, list):
|
|
170
|
+
errors.append(msg)
|
|
158
171
|
|
|
159
172
|
return result
|
|
160
173
|
|
|
161
174
|
|
|
175
|
+
def __post_for_token(url: str,
|
|
176
|
+
header_data: dict[str, str],
|
|
177
|
+
body_data: dict[str, Any],
|
|
178
|
+
errors: list[str] | None,
|
|
179
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
180
|
+
"""
|
|
181
|
+
Send a *POST* request to *url* and return the token data obtained.
|
|
182
|
+
|
|
183
|
+
Token acquisition and token refresh are the two types of requests contemplated herein.
|
|
184
|
+
For the former, *header_data* and *body_data* will have contents customized to the specific provider,
|
|
185
|
+
whereas the latter's *body_data* will contain these two attributes:
|
|
186
|
+
- "grant_type": "refresh_token"
|
|
187
|
+
- "refresh_token": <current-refresh-token>
|
|
188
|
+
|
|
189
|
+
The typical data set returned contains the following attributes:
|
|
190
|
+
{
|
|
191
|
+
"token_type": "Bearer",
|
|
192
|
+
"access_token": <str>,
|
|
193
|
+
"expires_in": <number-of-seconds>,
|
|
194
|
+
"refresh_token": <str>,
|
|
195
|
+
"refesh_expires_in": <number-of-seconds>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:param url: the target URL
|
|
199
|
+
:param header_data: the data to send in the header of the request
|
|
200
|
+
:param body_data: the data to send in the body of the request
|
|
201
|
+
:param errors: incidental errors
|
|
202
|
+
:param logger: optional logger
|
|
203
|
+
:return: the token data, or *None* if error
|
|
204
|
+
"""
|
|
205
|
+
# initialize the return variable
|
|
206
|
+
result: dict[str, Any] | None = None
|
|
207
|
+
|
|
208
|
+
# log the POST
|
|
209
|
+
if logger:
|
|
210
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
|
|
211
|
+
ensure_ascii=False)}")
|
|
212
|
+
try:
|
|
213
|
+
response: requests.Response = requests.post(url=url,
|
|
214
|
+
data=body_data,
|
|
215
|
+
headers=header_data,
|
|
216
|
+
timeout=None)
|
|
217
|
+
if response.status_code == 200:
|
|
218
|
+
# request succeeded
|
|
219
|
+
result = response.json()
|
|
220
|
+
if logger:
|
|
221
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
222
|
+
else:
|
|
223
|
+
# request failed, report the problem
|
|
224
|
+
msg: str = (f"POST failure, "
|
|
225
|
+
f"status {response.status_code}, reason {response.reason}")
|
|
226
|
+
if hasattr(response, "content") and response.content:
|
|
227
|
+
msg += f", content '{response.content}'"
|
|
228
|
+
if logger:
|
|
229
|
+
logger.error(msg=msg)
|
|
230
|
+
if isinstance(errors, list):
|
|
231
|
+
errors.append(msg)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
# the operation raised an exception
|
|
234
|
+
err_msg = exc_format(exc=e,
|
|
235
|
+
exc_info=sys.exc_info())
|
|
236
|
+
msg: str = f"POST error, {err_msg}"
|
|
237
|
+
if logger:
|
|
238
|
+
logger.debug(msg=msg)
|
|
239
|
+
if isinstance(errors, list):
|
|
240
|
+
errors.append(msg)
|
|
241
|
+
|
|
242
|
+
return result
|
pypomes_iam/token_pomes.py
CHANGED
|
@@ -7,6 +7,45 @@ 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,
|
|
12
51
|
recipient_id: str = None,
|
|
@@ -78,8 +117,6 @@ def token_validate(token: str,
|
|
|
78
117
|
"verify_nbf": False,
|
|
79
118
|
"verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
|
|
80
119
|
}
|
|
81
|
-
if issuer:
|
|
82
|
-
options["require"].append("iss")
|
|
83
120
|
try:
|
|
84
121
|
# raises:
|
|
85
122
|
# InvalidTokenError: token is invalid
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.9
|
|
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=0PC7Z7xsepxynmgjMvKZLjLdvC3s5kGk_dqCFrG4Qs8,42083
|
|
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.6.9.dist-info/METADATA,sha256=tPsAu2TWT9hb3vu-KHnYT77TYZytoCQfs78uxWLlLH8,661
|
|
9
|
+
pypomes_iam-0.6.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pypomes_iam-0.6.9.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
+
pypomes_iam-0.6.9.dist-info/RECORD,,
|