pypomes-iam 0.5.4__py3-none-any.whl → 0.5.6__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 +14 -6
- pypomes_iam/iam_actions.py +511 -0
- pypomes_iam/iam_common.py +1 -1
- pypomes_iam/iam_pomes.py +2 -506
- pypomes_iam/iam_services.py +23 -23
- pypomes_iam/jusbr_pomes.py +5 -5
- pypomes_iam/keycloak_pomes.py +5 -5
- pypomes_iam/provider_pomes.py +3 -3
- {pypomes_iam-0.5.4.dist-info → pypomes_iam-0.5.6.dist-info}/METADATA +1 -1
- pypomes_iam-0.5.6.dist-info/RECORD +13 -0
- pypomes_iam-0.5.4.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.4.dist-info → pypomes_iam-0.5.6.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.4.dist-info → pypomes_iam-0.5.6.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/__init__.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
from .iam_actions import (
|
|
2
|
+
action_callback, action_exchange,
|
|
3
|
+
action_login, action_logout, action_token
|
|
4
|
+
)
|
|
5
|
+
from .iam_common import (
|
|
6
|
+
IamServer
|
|
7
|
+
)
|
|
1
8
|
from .iam_pomes import (
|
|
2
|
-
|
|
3
|
-
login_callback, token_exchange,
|
|
4
|
-
user_login, user_logout, user_token
|
|
9
|
+
jwt_required
|
|
5
10
|
)
|
|
6
11
|
from .iam_services import (
|
|
7
12
|
logger_register
|
|
@@ -20,10 +25,13 @@ from .token_pomes import (
|
|
|
20
25
|
)
|
|
21
26
|
|
|
22
27
|
__all__ = [
|
|
23
|
-
#
|
|
28
|
+
# iam_actions
|
|
29
|
+
"action_callback", "action_exchange",
|
|
30
|
+
"action_login", "action_logout", "action_token",
|
|
31
|
+
# iam_commons
|
|
24
32
|
"IamServer",
|
|
25
|
-
|
|
26
|
-
"
|
|
33
|
+
# iam_pomes
|
|
34
|
+
"jwt_required",
|
|
27
35
|
# iam_services
|
|
28
36
|
"logger_register",
|
|
29
37
|
# jusbr_pomes
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from logging import Logger
|
|
8
|
+
from pypomes_core import TZ_LOCAL, exc_format
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .iam_common import (
|
|
12
|
+
IamServer, _iam_lock,
|
|
13
|
+
_get_iam_users, _get_iam_registry, # _get_public_key,
|
|
14
|
+
_get_login_timeout, _get_user_data
|
|
15
|
+
)
|
|
16
|
+
from .token_pomes import token_validate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def action_login(iam_server: IamServer,
|
|
20
|
+
args: dict[str, Any],
|
|
21
|
+
errors: list[str] = None,
|
|
22
|
+
logger: Logger = None) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Build the URL for redirecting the request to *iam_server*'s authentication page.
|
|
25
|
+
|
|
26
|
+
These are the expected attributes in *args*:
|
|
27
|
+
- user-id: optional, identifies the reference user (alias: 'login')
|
|
28
|
+
- redirect-uri: a parameter to be added to the query part of the returned URL
|
|
29
|
+
|
|
30
|
+
If provided, the user identification will be validated against the authorization data
|
|
31
|
+
returned by *iam_server* upon login. On success, the appropriate URL for invoking
|
|
32
|
+
the IAM server's authentication page is returned.
|
|
33
|
+
|
|
34
|
+
:param iam_server: the reference registered *IAM* server
|
|
35
|
+
:param args: the arguments passed when requesting the service
|
|
36
|
+
:param errors: incidental error messages
|
|
37
|
+
:param logger: optional logger
|
|
38
|
+
:return: the callback URL, with the appropriate parameters, of *None* if error
|
|
39
|
+
"""
|
|
40
|
+
# initialize the return variable
|
|
41
|
+
result: str | None = None
|
|
42
|
+
|
|
43
|
+
# obtain the optional user's identification
|
|
44
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
45
|
+
|
|
46
|
+
# build the user data
|
|
47
|
+
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
48
|
+
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
49
|
+
|
|
50
|
+
with _iam_lock:
|
|
51
|
+
# retrieve the user data from the IAM server's registry
|
|
52
|
+
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
53
|
+
user_id=oauth_state,
|
|
54
|
+
errors=errors,
|
|
55
|
+
logger=logger)
|
|
56
|
+
if user_data:
|
|
57
|
+
user_data["login-id"] = user_id
|
|
58
|
+
timeout: int = _get_login_timeout(iam_server=iam_server,
|
|
59
|
+
errors=errors,
|
|
60
|
+
logger=logger)
|
|
61
|
+
if not errors:
|
|
62
|
+
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
|
|
63
|
+
if timeout else None
|
|
64
|
+
redirect_uri: str = args.get("redirect-uri")
|
|
65
|
+
user_data["redirect-uri"] = redirect_uri
|
|
66
|
+
|
|
67
|
+
# build the login url
|
|
68
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
69
|
+
errors=errors,
|
|
70
|
+
logger=logger)
|
|
71
|
+
if registry:
|
|
72
|
+
result = (f"{registry["base-url"]}/protocol/openid-connect/auth"
|
|
73
|
+
f"?response_type=code&scope=openid"
|
|
74
|
+
f"&client_id={registry["client-id"]}"
|
|
75
|
+
f"&redirect_uri={redirect_uri}"
|
|
76
|
+
f"&state={oauth_state}")
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def action_logout(iam_server: IamServer,
|
|
81
|
+
args: dict[str, Any],
|
|
82
|
+
errors: list[str] = None,
|
|
83
|
+
logger: Logger = None) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Logout the user, by removing all data associating it from *iam_server*'s registry.
|
|
86
|
+
|
|
87
|
+
The user is identified by the attribute *user-id* or "login", provided in *args*.
|
|
88
|
+
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
89
|
+
Otherwise, this operation fails silently, unless an error has ocurred.
|
|
90
|
+
|
|
91
|
+
:param iam_server: the reference registered *IAM* server
|
|
92
|
+
:param args: the arguments passed when requesting the service
|
|
93
|
+
:param errors: incidental error messages
|
|
94
|
+
:param logger: optional logger
|
|
95
|
+
"""
|
|
96
|
+
# obtain the user's identification
|
|
97
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
98
|
+
|
|
99
|
+
if user_id:
|
|
100
|
+
with _iam_lock:
|
|
101
|
+
# retrieve the data for all users in the IAM server's registry
|
|
102
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
103
|
+
errors=errors,
|
|
104
|
+
logger=logger) or {}
|
|
105
|
+
if user_id in users:
|
|
106
|
+
users.pop(user_id)
|
|
107
|
+
if logger:
|
|
108
|
+
logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def action_token(iam_server: IamServer,
|
|
112
|
+
args: dict[str, Any],
|
|
113
|
+
errors: list[str] = None,
|
|
114
|
+
logger: Logger = None) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Retrieve the authentication token for the user, from *iam_server*.
|
|
117
|
+
|
|
118
|
+
The user is identified by the attribute *user-id* or *login*, provided in *args*.
|
|
119
|
+
|
|
120
|
+
:param iam_server: the reference registered *IAM* server
|
|
121
|
+
:param args: the arguments passed when requesting the service
|
|
122
|
+
:param errors: incidental error messages
|
|
123
|
+
:param logger: optional logger
|
|
124
|
+
:return: the token for user indicated, or *None* if error
|
|
125
|
+
"""
|
|
126
|
+
# initialize the return variable
|
|
127
|
+
result: str | None = None
|
|
128
|
+
|
|
129
|
+
# obtain the user's identification
|
|
130
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
131
|
+
|
|
132
|
+
err_msg: str | None = None
|
|
133
|
+
if user_id:
|
|
134
|
+
with _iam_lock:
|
|
135
|
+
# retrieve the user data in the IAM server's registry
|
|
136
|
+
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
137
|
+
user_id=user_id,
|
|
138
|
+
errors=errors,
|
|
139
|
+
logger=logger)
|
|
140
|
+
token: str = user_data["access-token"] if user_data else None
|
|
141
|
+
if token:
|
|
142
|
+
access_expiration: int = user_data.get("access-expiration")
|
|
143
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
144
|
+
if now < access_expiration:
|
|
145
|
+
result = token
|
|
146
|
+
else:
|
|
147
|
+
# access token has expired
|
|
148
|
+
refresh_token: str = user_data["refresh-token"]
|
|
149
|
+
if refresh_token:
|
|
150
|
+
refresh_expiration = user_data["refresh-expiration"]
|
|
151
|
+
if now < refresh_expiration:
|
|
152
|
+
body_data: dict[str, str] = {
|
|
153
|
+
"grant_type": "refresh_token",
|
|
154
|
+
"refresh_token": refresh_token
|
|
155
|
+
}
|
|
156
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
157
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
158
|
+
body_data=body_data,
|
|
159
|
+
errors=errors,
|
|
160
|
+
logger=logger)
|
|
161
|
+
# validate and store the token data
|
|
162
|
+
if token_data:
|
|
163
|
+
token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
|
|
164
|
+
user_data=user_data,
|
|
165
|
+
token_data=token_data,
|
|
166
|
+
now=now,
|
|
167
|
+
errors=errors,
|
|
168
|
+
logger=logger)
|
|
169
|
+
result = token_info[1]
|
|
170
|
+
else:
|
|
171
|
+
# refresh token is no longer valid
|
|
172
|
+
user_data["refresh-token"] = None
|
|
173
|
+
else:
|
|
174
|
+
# refresh token has expired
|
|
175
|
+
err_msg = "Access and refresh tokens expired"
|
|
176
|
+
if logger:
|
|
177
|
+
logger.error(msg=err_msg)
|
|
178
|
+
else:
|
|
179
|
+
err_msg = "Access token expired, no refresh token available"
|
|
180
|
+
if logger:
|
|
181
|
+
logger.error(msg=err_msg)
|
|
182
|
+
else:
|
|
183
|
+
err_msg = f"User '{user_id}' not authenticated"
|
|
184
|
+
if logger:
|
|
185
|
+
logger.error(msg=err_msg)
|
|
186
|
+
else:
|
|
187
|
+
err_msg = "User identification not provided"
|
|
188
|
+
if logger:
|
|
189
|
+
logger.error(msg=err_msg)
|
|
190
|
+
|
|
191
|
+
if err_msg and isinstance(errors, list):
|
|
192
|
+
errors.append(err_msg)
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def action_callback(iam_server: IamServer,
|
|
198
|
+
args: dict[str, Any],
|
|
199
|
+
errors: list[str] = None,
|
|
200
|
+
logger: Logger = None) -> tuple[str, str] | None:
|
|
201
|
+
"""
|
|
202
|
+
Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
|
|
203
|
+
|
|
204
|
+
The relevant expected arguments in *args* are:
|
|
205
|
+
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
206
|
+
- *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
|
|
207
|
+
|
|
208
|
+
:param iam_server: the reference registered *IAM* server
|
|
209
|
+
:param args: the arguments passed when requesting the service
|
|
210
|
+
:param errors: incidental errors
|
|
211
|
+
:param logger: optional logger
|
|
212
|
+
:return: a tuple containing the reference user identification and the token obtained, or *None* if error
|
|
213
|
+
"""
|
|
214
|
+
# initialize the return variable
|
|
215
|
+
result: tuple[str, str] | None = None
|
|
216
|
+
|
|
217
|
+
with _iam_lock:
|
|
218
|
+
# retrieve the IAM server's data for all users
|
|
219
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
220
|
+
errors=errors,
|
|
221
|
+
logger=logger) or {}
|
|
222
|
+
# retrieve the OAuth2 state
|
|
223
|
+
oauth_state: str = args.get("state")
|
|
224
|
+
user_data: dict[str, Any] | None = None
|
|
225
|
+
if oauth_state:
|
|
226
|
+
for user, data in users.items():
|
|
227
|
+
if user == oauth_state:
|
|
228
|
+
user_data = data
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
# exchange 'code' received for the token
|
|
232
|
+
if user_data:
|
|
233
|
+
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
234
|
+
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
235
|
+
errors.append("Operation timeout")
|
|
236
|
+
else:
|
|
237
|
+
users.pop(oauth_state)
|
|
238
|
+
code: str = args.get("code")
|
|
239
|
+
body_data: dict[str, Any] = {
|
|
240
|
+
"grant_type": "authorization_code",
|
|
241
|
+
"code": code,
|
|
242
|
+
"redirect_uri": user_data.pop("redirect-uri")
|
|
243
|
+
}
|
|
244
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
245
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
246
|
+
body_data=body_data,
|
|
247
|
+
errors=errors,
|
|
248
|
+
logger=logger)
|
|
249
|
+
# validate and store the token data
|
|
250
|
+
if token_data:
|
|
251
|
+
result = __validate_and_store(iam_server=iam_server,
|
|
252
|
+
user_data=user_data,
|
|
253
|
+
token_data=token_data,
|
|
254
|
+
now=now,
|
|
255
|
+
errors=errors,
|
|
256
|
+
logger=logger)
|
|
257
|
+
else:
|
|
258
|
+
msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
|
|
259
|
+
if logger:
|
|
260
|
+
logger.error(msg=msg)
|
|
261
|
+
if isinstance(errors, list):
|
|
262
|
+
errors.append(msg)
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def action_exchange(iam_server: IamServer,
|
|
268
|
+
args: dict[str, Any],
|
|
269
|
+
errors: list[str] = None,
|
|
270
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
271
|
+
"""
|
|
272
|
+
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
273
|
+
|
|
274
|
+
The expected parameters in *args* are:
|
|
275
|
+
- user-id: identification for the reference user (alias: 'login')
|
|
276
|
+
- token: the token to be exchanged
|
|
277
|
+
|
|
278
|
+
The typical data set returned contains the following attributes:
|
|
279
|
+
{
|
|
280
|
+
"token_type": "Bearer",
|
|
281
|
+
"access_token": <str>,
|
|
282
|
+
"expires_in": <number-of-seconds>,
|
|
283
|
+
"refresh_token": <str>,
|
|
284
|
+
"refesh_expires_in": <number-of-seconds>
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
:param iam_server: the reference registered *IAM* server
|
|
288
|
+
:param args: the arguments passed when requesting the service
|
|
289
|
+
:param errors: incidental errors
|
|
290
|
+
:param logger: optional logger
|
|
291
|
+
:return: the data for the new token, or *None* if error
|
|
292
|
+
"""
|
|
293
|
+
# initialize the return variable
|
|
294
|
+
result: dict[str, Any] | None = None
|
|
295
|
+
|
|
296
|
+
# obtain the user's identification
|
|
297
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
298
|
+
|
|
299
|
+
# obtain the token to be exchanged
|
|
300
|
+
token: str = args.get("access-token")
|
|
301
|
+
|
|
302
|
+
if user_id and token:
|
|
303
|
+
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
304
|
+
with _iam_lock:
|
|
305
|
+
# retrieve the IAM server's registry
|
|
306
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
307
|
+
errors=errors,
|
|
308
|
+
logger=logger)
|
|
309
|
+
if registry:
|
|
310
|
+
body_data: dict[str, str] = {
|
|
311
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
312
|
+
"subject_token": token,
|
|
313
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
314
|
+
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
315
|
+
"audience": registry["client-id"],
|
|
316
|
+
"subject_issuer": "oidc"
|
|
317
|
+
}
|
|
318
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
319
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
|
|
320
|
+
body_data=body_data,
|
|
321
|
+
errors=errors,
|
|
322
|
+
logger=logger)
|
|
323
|
+
# validate and store the token data
|
|
324
|
+
if token_data:
|
|
325
|
+
user_data: dict[str, Any] = {}
|
|
326
|
+
result = __validate_and_store(iam_server=iam_server,
|
|
327
|
+
user_data=user_data,
|
|
328
|
+
token_data=token_data,
|
|
329
|
+
now=now,
|
|
330
|
+
errors=errors,
|
|
331
|
+
logger=logger)
|
|
332
|
+
else:
|
|
333
|
+
msg: str = "User identification or token not provided"
|
|
334
|
+
if logger:
|
|
335
|
+
logger.error(msg=msg)
|
|
336
|
+
if isinstance(errors, list):
|
|
337
|
+
errors.append(msg)
|
|
338
|
+
|
|
339
|
+
return result
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def __post_for_token(iam_server: IamServer,
|
|
343
|
+
body_data: dict[str, Any],
|
|
344
|
+
errors: list[str] | None,
|
|
345
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
346
|
+
"""
|
|
347
|
+
Send a POST request to obtain the authentication token data, and return the data received.
|
|
348
|
+
|
|
349
|
+
For token acquisition, *body_data* will have the attributes:
|
|
350
|
+
- "grant_type": "authorization_code"
|
|
351
|
+
- "code": <16-character-random-code>
|
|
352
|
+
- "redirect_uri": <redirect-uri>
|
|
353
|
+
|
|
354
|
+
For token refresh, *body_data* will have the attributes:
|
|
355
|
+
- "grant_type": "refresh_token"
|
|
356
|
+
- "refresh_token": <current-refresh-token>
|
|
357
|
+
|
|
358
|
+
For token exchange, *body_data* will have the attributes:
|
|
359
|
+
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
360
|
+
- "subject_token": <token-to-be-exchanged>,
|
|
361
|
+
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
362
|
+
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
363
|
+
- "audience": <client-id>,
|
|
364
|
+
- "subject_issuer": "oidc"
|
|
365
|
+
|
|
366
|
+
These attributes are then added to *body_data*:
|
|
367
|
+
- "client_id": <client-id>,
|
|
368
|
+
- "client_secret": <client-secret>,
|
|
369
|
+
|
|
370
|
+
If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
371
|
+
Otherwise, *errors* will contain the appropriate error message.
|
|
372
|
+
|
|
373
|
+
The typical data set returned contains the following attributes:
|
|
374
|
+
{
|
|
375
|
+
"token_type": "Bearer",
|
|
376
|
+
"access_token": <str>,
|
|
377
|
+
"expires_in": <number-of-seconds>,
|
|
378
|
+
"refresh_token": <str>,
|
|
379
|
+
"refesh_expires_in": <number-of-seconds>
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
:param iam_server: the reference registered *IAM* server
|
|
383
|
+
:param body_data: the data to send in the body of the request
|
|
384
|
+
:param errors: incidental errors
|
|
385
|
+
:param logger: optional logger
|
|
386
|
+
:return: the token data, or *None* if error
|
|
387
|
+
"""
|
|
388
|
+
# initialize the return variable
|
|
389
|
+
result: dict[str, Any] | None = None
|
|
390
|
+
|
|
391
|
+
err_msg: str | None = None
|
|
392
|
+
with _iam_lock:
|
|
393
|
+
# retrieve the IAM server's registry
|
|
394
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
395
|
+
errors=errors,
|
|
396
|
+
logger=logger)
|
|
397
|
+
if registry:
|
|
398
|
+
# complete the data to send in body of request
|
|
399
|
+
body_data["client_id"] = registry["client-id"]
|
|
400
|
+
client_secret: str = registry["client-secret"]
|
|
401
|
+
|
|
402
|
+
# obtain the token
|
|
403
|
+
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
404
|
+
|
|
405
|
+
# log the POST ('client_secret' data must not be shown in log)
|
|
406
|
+
if logger:
|
|
407
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
|
|
408
|
+
ensure_ascii=False)}")
|
|
409
|
+
if client_secret:
|
|
410
|
+
body_data["client_secret"] = client_secret
|
|
411
|
+
try:
|
|
412
|
+
# typical return on a token request:
|
|
413
|
+
# {
|
|
414
|
+
# "token_type": "Bearer",
|
|
415
|
+
# "access_token": <str>,
|
|
416
|
+
# "expires_in": <number-of-seconds>,
|
|
417
|
+
# "refresh_token": <str>,
|
|
418
|
+
# "refesh_expires_in": <number-of-seconds>
|
|
419
|
+
# }
|
|
420
|
+
response: requests.Response = requests.post(url=url,
|
|
421
|
+
data=body_data)
|
|
422
|
+
if response.status_code == 200:
|
|
423
|
+
# request succeeded
|
|
424
|
+
result = response.json()
|
|
425
|
+
if logger:
|
|
426
|
+
logger.debug(msg=f"POST success, {json.dumps(obj=result,
|
|
427
|
+
ensure_ascii=False)}")
|
|
428
|
+
else:
|
|
429
|
+
# request resulted in error
|
|
430
|
+
err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
431
|
+
if hasattr(response, "content") and response.content:
|
|
432
|
+
err_msg += f", content '{response.content}'"
|
|
433
|
+
if logger:
|
|
434
|
+
logger.error(msg=err_msg)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
# the operation raised an exception
|
|
437
|
+
err_msg = exc_format(exc=e,
|
|
438
|
+
exc_info=sys.exc_info())
|
|
439
|
+
if logger:
|
|
440
|
+
logger.error(msg=err_msg)
|
|
441
|
+
|
|
442
|
+
if err_msg and isinstance(errors, list):
|
|
443
|
+
errors.append(err_msg)
|
|
444
|
+
|
|
445
|
+
return result
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def __validate_and_store(iam_server: IamServer,
|
|
449
|
+
user_data: dict[str, Any],
|
|
450
|
+
token_data: dict[str, Any],
|
|
451
|
+
now: int,
|
|
452
|
+
errors: list[str] | None,
|
|
453
|
+
logger: Logger) -> tuple[str, str] | None:
|
|
454
|
+
"""
|
|
455
|
+
Validate and store the token data.
|
|
456
|
+
|
|
457
|
+
The typical *token_data* contains the following attributes:
|
|
458
|
+
{
|
|
459
|
+
"token_type": "Bearer",
|
|
460
|
+
"access_token": <str>,
|
|
461
|
+
"expires_in": <number-of-seconds>,
|
|
462
|
+
"refresh_token": <str>,
|
|
463
|
+
"refesh_expires_in": <number-of-seconds>
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
:param iam_server: the reference registered *IAM* server
|
|
467
|
+
:param user_data: the aurthentication data kepth in *iam_server*'s registry
|
|
468
|
+
:param token_data: the token data
|
|
469
|
+
:param errors: incidental errors
|
|
470
|
+
:param logger: optional logger
|
|
471
|
+
:return: tuple containing the user identification and the validated and stored token, or *None* if error
|
|
472
|
+
"""
|
|
473
|
+
# initialize the return variable
|
|
474
|
+
result: tuple[str, str] | None = None
|
|
475
|
+
|
|
476
|
+
with _iam_lock:
|
|
477
|
+
# retrieve the IAM server's registry
|
|
478
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
479
|
+
errors=errors,
|
|
480
|
+
logger=logger)
|
|
481
|
+
if registry:
|
|
482
|
+
token: str = token_data.get("access_token")
|
|
483
|
+
user_data["access-token"] = token
|
|
484
|
+
# keep current refresh token if a new one is not provided
|
|
485
|
+
if token_data.get("refresh_token"):
|
|
486
|
+
user_data["refresh-token"] = token_data.get("refresh_token")
|
|
487
|
+
user_data["access-expiration"] = now + token_data.get("expires_in")
|
|
488
|
+
refresh_exp: int = user_data.get("refresh_expires_in")
|
|
489
|
+
user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
|
|
490
|
+
# public_key: str = _get_public_key(iam_server=iam_server,
|
|
491
|
+
# errors=errors,
|
|
492
|
+
# logger=logger)
|
|
493
|
+
recipient_attr = registry["recipient-attr"]
|
|
494
|
+
login_id = user_data.pop("login-id", None)
|
|
495
|
+
claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
496
|
+
issuer=registry["base-url"],
|
|
497
|
+
recipient_id=login_id,
|
|
498
|
+
recipient_attr=recipient_attr,
|
|
499
|
+
# public_key=public_key,
|
|
500
|
+
errors=errors,
|
|
501
|
+
logger=logger)
|
|
502
|
+
if claims:
|
|
503
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
504
|
+
errors=errors,
|
|
505
|
+
logger=logger)
|
|
506
|
+
# must test with 'not errors'
|
|
507
|
+
if not errors:
|
|
508
|
+
user_id: str = login_id if login_id else claims["payload"][recipient_attr]
|
|
509
|
+
users[user_id] = user_data
|
|
510
|
+
result = (user_id, token)
|
|
511
|
+
return result
|
pypomes_iam/iam_common.py
CHANGED
|
@@ -50,7 +50,7 @@ class IamServer(StrEnum):
|
|
|
50
50
|
# }
|
|
51
51
|
_IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
|
|
52
52
|
|
|
53
|
-
# the lock protecting the data in '
|
|
53
|
+
# the lock protecting the data in '_IAM_SERVERS'
|
|
54
54
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
55
55
|
_iam_lock: Final[RLock] = RLock()
|
|
56
56
|
|
pypomes_iam/iam_pomes.py
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import requests
|
|
3
|
-
import secrets
|
|
4
|
-
import string
|
|
5
|
-
import sys
|
|
6
|
-
from datetime import datetime
|
|
7
1
|
from flask import Request, Response, request
|
|
8
|
-
from logging import Logger
|
|
9
|
-
from pypomes_core import TZ_LOCAL, exc_format
|
|
10
2
|
from typing import Any
|
|
11
3
|
|
|
12
4
|
from .iam_common import (
|
|
13
|
-
IamServer, _iam_lock,
|
|
14
|
-
|
|
15
|
-
_get_login_timeout, _get_user_data, _iam_server_from_issuer
|
|
5
|
+
IamServer, _iam_lock, _get_iam_registry,
|
|
6
|
+
_iam_server_from_issuer # _get_public_key
|
|
16
7
|
)
|
|
17
8
|
from .token_pomes import token_get_claims, token_validate
|
|
18
9
|
|
|
@@ -34,329 +25,6 @@ def jwt_required(func: callable) -> callable:
|
|
|
34
25
|
return wrapper
|
|
35
26
|
|
|
36
27
|
|
|
37
|
-
def user_login(iam_server: IamServer,
|
|
38
|
-
args: dict[str, Any],
|
|
39
|
-
errors: list[str] = None,
|
|
40
|
-
logger: Logger = None) -> str:
|
|
41
|
-
"""
|
|
42
|
-
Build the URL for redirecting the request to *iam_server*'s authentication page.
|
|
43
|
-
|
|
44
|
-
These are the expected attributes in *args*:
|
|
45
|
-
- user-id: optional, identifies the reference user (alias: 'login')
|
|
46
|
-
- redirect-uri: a parameter to be added to the query part of the returned URL
|
|
47
|
-
|
|
48
|
-
If provided, the user identification will be validated against the authorization data
|
|
49
|
-
returned by *iam_server* upon login. On success, the appropriate URL for invoking
|
|
50
|
-
the IAM server's authentication page is returned.
|
|
51
|
-
|
|
52
|
-
:param iam_server: the reference registered *IAM* server
|
|
53
|
-
:param args: the arguments passed when requesting the service
|
|
54
|
-
:param errors: incidental error messages
|
|
55
|
-
:param logger: optional logger
|
|
56
|
-
:return: the callback URL, with the appropriate parameters, of *None* if error
|
|
57
|
-
"""
|
|
58
|
-
# initialize the return variable
|
|
59
|
-
result: str | None = None
|
|
60
|
-
|
|
61
|
-
# obtain the optional user's identification
|
|
62
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
63
|
-
|
|
64
|
-
# build the user data
|
|
65
|
-
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
66
|
-
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
67
|
-
|
|
68
|
-
with _iam_lock:
|
|
69
|
-
# retrieve the user data from the IAM server's registry
|
|
70
|
-
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
71
|
-
user_id=oauth_state,
|
|
72
|
-
errors=errors,
|
|
73
|
-
logger=logger)
|
|
74
|
-
if user_data:
|
|
75
|
-
user_data["login-id"] = user_id
|
|
76
|
-
timeout: int = _get_login_timeout(iam_server=iam_server,
|
|
77
|
-
errors=errors,
|
|
78
|
-
logger=logger)
|
|
79
|
-
if not errors:
|
|
80
|
-
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
|
|
81
|
-
if timeout else None
|
|
82
|
-
redirect_uri: str = args.get("redirect-uri")
|
|
83
|
-
user_data["redirect-uri"] = redirect_uri
|
|
84
|
-
|
|
85
|
-
# build the login url
|
|
86
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
87
|
-
errors=errors,
|
|
88
|
-
logger=logger)
|
|
89
|
-
if registry:
|
|
90
|
-
result = (f"{registry["base-url"]}/protocol/openid-connect/auth"
|
|
91
|
-
f"?response_type=code&scope=openid"
|
|
92
|
-
f"&client_id={registry["client-id"]}"
|
|
93
|
-
f"&redirect_uri={redirect_uri}"
|
|
94
|
-
f"&state={oauth_state}")
|
|
95
|
-
return result
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def user_logout(iam_server: IamServer,
|
|
99
|
-
args: dict[str, Any],
|
|
100
|
-
errors: list[str] = None,
|
|
101
|
-
logger: Logger = None) -> None:
|
|
102
|
-
"""
|
|
103
|
-
Logout the user, by removing all data associating it from *iam_server*'s registry.
|
|
104
|
-
|
|
105
|
-
The user is identified by the attribute *user-id* or "login", provided in *args*.
|
|
106
|
-
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
107
|
-
Otherwise, this operation fails silently, unless an error has ocurred.
|
|
108
|
-
|
|
109
|
-
:param iam_server: the reference registered *IAM* server
|
|
110
|
-
:param args: the arguments passed when requesting the service
|
|
111
|
-
:param errors: incidental error messages
|
|
112
|
-
:param logger: optional logger
|
|
113
|
-
"""
|
|
114
|
-
# obtain the user's identification
|
|
115
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
116
|
-
|
|
117
|
-
if user_id:
|
|
118
|
-
with _iam_lock:
|
|
119
|
-
# retrieve the data for all users in the IAM server's registry
|
|
120
|
-
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
121
|
-
errors=errors,
|
|
122
|
-
logger=logger) or {}
|
|
123
|
-
if user_id in users:
|
|
124
|
-
users.pop(user_id)
|
|
125
|
-
if logger:
|
|
126
|
-
logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def user_token(iam_server: IamServer,
|
|
130
|
-
args: dict[str, Any],
|
|
131
|
-
errors: list[str] = None,
|
|
132
|
-
logger: Logger = None) -> str:
|
|
133
|
-
"""
|
|
134
|
-
Retrieve the authentication token for the user, from *iam_server*.
|
|
135
|
-
|
|
136
|
-
The user is identified by the attribute *user-id* or *login*, provided in *args*.
|
|
137
|
-
|
|
138
|
-
:param iam_server: the reference registered *IAM* server
|
|
139
|
-
:param args: the arguments passed when requesting the service
|
|
140
|
-
:param errors: incidental error messages
|
|
141
|
-
:param logger: optional logger
|
|
142
|
-
:return: the token for user indicated, or *None* if error
|
|
143
|
-
"""
|
|
144
|
-
# initialize the return variable
|
|
145
|
-
result: str | None = None
|
|
146
|
-
|
|
147
|
-
# obtain the user's identification
|
|
148
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
149
|
-
|
|
150
|
-
err_msg: str | None = None
|
|
151
|
-
if user_id:
|
|
152
|
-
with _iam_lock:
|
|
153
|
-
# retrieve the user data in the IAM server's registry
|
|
154
|
-
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
155
|
-
user_id=user_id,
|
|
156
|
-
errors=errors,
|
|
157
|
-
logger=logger)
|
|
158
|
-
token: str = user_data["access-token"] if user_data else None
|
|
159
|
-
if token:
|
|
160
|
-
access_expiration: int = user_data.get("access-expiration")
|
|
161
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
162
|
-
if now < access_expiration:
|
|
163
|
-
result = token
|
|
164
|
-
else:
|
|
165
|
-
# access token has expired
|
|
166
|
-
refresh_token: str = user_data["refresh-token"]
|
|
167
|
-
if refresh_token:
|
|
168
|
-
refresh_expiration = user_data["refresh-expiration"]
|
|
169
|
-
if now < refresh_expiration:
|
|
170
|
-
body_data: dict[str, str] = {
|
|
171
|
-
"grant_type": "refresh_token",
|
|
172
|
-
"refresh_token": refresh_token
|
|
173
|
-
}
|
|
174
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
175
|
-
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
176
|
-
body_data=body_data,
|
|
177
|
-
errors=errors,
|
|
178
|
-
logger=logger)
|
|
179
|
-
# validate and store the token data
|
|
180
|
-
if token_data:
|
|
181
|
-
token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
|
|
182
|
-
user_data=user_data,
|
|
183
|
-
token_data=token_data,
|
|
184
|
-
now=now,
|
|
185
|
-
errors=errors,
|
|
186
|
-
logger=logger)
|
|
187
|
-
result = token_info[1]
|
|
188
|
-
else:
|
|
189
|
-
# refresh token is no longer valid
|
|
190
|
-
user_data["refresh-token"] = None
|
|
191
|
-
else:
|
|
192
|
-
# refresh token has expired
|
|
193
|
-
err_msg = "Access and refresh tokens expired"
|
|
194
|
-
if logger:
|
|
195
|
-
logger.error(msg=err_msg)
|
|
196
|
-
else:
|
|
197
|
-
err_msg = "Access token expired, no refresh token available"
|
|
198
|
-
if logger:
|
|
199
|
-
logger.error(msg=err_msg)
|
|
200
|
-
else:
|
|
201
|
-
err_msg = f"User '{user_id}' not authenticated"
|
|
202
|
-
if logger:
|
|
203
|
-
logger.error(msg=err_msg)
|
|
204
|
-
else:
|
|
205
|
-
err_msg = "User identification not provided"
|
|
206
|
-
if logger:
|
|
207
|
-
logger.error(msg=err_msg)
|
|
208
|
-
|
|
209
|
-
if err_msg and isinstance(errors, list):
|
|
210
|
-
errors.append(err_msg)
|
|
211
|
-
|
|
212
|
-
return result
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def login_callback(iam_server: IamServer,
|
|
216
|
-
args: dict[str, Any],
|
|
217
|
-
errors: list[str] = None,
|
|
218
|
-
logger: Logger = None) -> tuple[str, str] | None:
|
|
219
|
-
"""
|
|
220
|
-
Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
|
|
221
|
-
|
|
222
|
-
The relevant expected arguments in *args* are:
|
|
223
|
-
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
224
|
-
- *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
|
|
225
|
-
|
|
226
|
-
:param iam_server: the reference registered *IAM* server
|
|
227
|
-
:param args: the arguments passed when requesting the service
|
|
228
|
-
:param errors: incidental errors
|
|
229
|
-
:param logger: optional logger
|
|
230
|
-
:return: a tuple containing the reference user identification and the token obtained, or *None* if error
|
|
231
|
-
"""
|
|
232
|
-
# initialize the return variable
|
|
233
|
-
result: tuple[str, str] | None = None
|
|
234
|
-
|
|
235
|
-
with _iam_lock:
|
|
236
|
-
# retrieve the IAM server's data for all users
|
|
237
|
-
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
238
|
-
errors=errors,
|
|
239
|
-
logger=logger) or {}
|
|
240
|
-
# retrieve the OAuth2 state
|
|
241
|
-
oauth_state: str = args.get("state")
|
|
242
|
-
user_data: dict[str, Any] | None = None
|
|
243
|
-
if oauth_state:
|
|
244
|
-
for user, data in users.items():
|
|
245
|
-
if user == oauth_state:
|
|
246
|
-
user_data = data
|
|
247
|
-
break
|
|
248
|
-
|
|
249
|
-
# exchange 'code' received for the token
|
|
250
|
-
if user_data:
|
|
251
|
-
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
252
|
-
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
253
|
-
errors.append("Operation timeout")
|
|
254
|
-
else:
|
|
255
|
-
users.pop(oauth_state)
|
|
256
|
-
code: str = args.get("code")
|
|
257
|
-
body_data: dict[str, Any] = {
|
|
258
|
-
"grant_type": "authorization_code",
|
|
259
|
-
"code": code,
|
|
260
|
-
"redirect_uri": user_data.pop("redirect-uri")
|
|
261
|
-
}
|
|
262
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
263
|
-
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
264
|
-
body_data=body_data,
|
|
265
|
-
errors=errors,
|
|
266
|
-
logger=logger)
|
|
267
|
-
# validate and store the token data
|
|
268
|
-
if token_data:
|
|
269
|
-
result = __validate_and_store(iam_server=iam_server,
|
|
270
|
-
user_data=user_data,
|
|
271
|
-
token_data=token_data,
|
|
272
|
-
now=now,
|
|
273
|
-
errors=errors,
|
|
274
|
-
logger=logger)
|
|
275
|
-
else:
|
|
276
|
-
msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
|
|
277
|
-
if logger:
|
|
278
|
-
logger.error(msg=msg)
|
|
279
|
-
if isinstance(errors, list):
|
|
280
|
-
errors.append(msg)
|
|
281
|
-
|
|
282
|
-
return result
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def token_exchange(iam_server: IamServer,
|
|
286
|
-
args: dict[str, Any],
|
|
287
|
-
errors: list[str] = None,
|
|
288
|
-
logger: Logger = None) -> dict[str, Any]:
|
|
289
|
-
"""
|
|
290
|
-
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
291
|
-
|
|
292
|
-
The expected parameters in *args* are:
|
|
293
|
-
- user-id: identification for the reference user (alias: 'login')
|
|
294
|
-
- token: the token to be exchanged
|
|
295
|
-
|
|
296
|
-
The typical data set returned contains the following attributes:
|
|
297
|
-
{
|
|
298
|
-
"token_type": "Bearer",
|
|
299
|
-
"access_token": <str>,
|
|
300
|
-
"expires_in": <number-of-seconds>,
|
|
301
|
-
"refresh_token": <str>,
|
|
302
|
-
"refesh_expires_in": <number-of-seconds>
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
:param iam_server: the reference registered *IAM* server
|
|
306
|
-
:param args: the arguments passed when requesting the service
|
|
307
|
-
:param errors: incidental errors
|
|
308
|
-
:param logger: optional logger
|
|
309
|
-
:return: the data for the new token, or *None* if error
|
|
310
|
-
"""
|
|
311
|
-
# initialize the return variable
|
|
312
|
-
result: dict[str, Any] | None = None
|
|
313
|
-
|
|
314
|
-
# obtain the user's identification
|
|
315
|
-
user_id: str = args.get("user-id") or args.get("login")
|
|
316
|
-
|
|
317
|
-
# obtain the token to be exchanged
|
|
318
|
-
token: str = args.get("access-token")
|
|
319
|
-
|
|
320
|
-
if user_id and token:
|
|
321
|
-
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
322
|
-
with _iam_lock:
|
|
323
|
-
# retrieve the IAM server's registry
|
|
324
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
325
|
-
errors=errors,
|
|
326
|
-
logger=logger)
|
|
327
|
-
if registry:
|
|
328
|
-
body_data: dict[str, str] = {
|
|
329
|
-
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
330
|
-
"subject_token": token,
|
|
331
|
-
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
332
|
-
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
333
|
-
"audience": registry["client-id"],
|
|
334
|
-
"subject_issuer": "oidc"
|
|
335
|
-
}
|
|
336
|
-
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
337
|
-
token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
|
|
338
|
-
body_data=body_data,
|
|
339
|
-
errors=errors,
|
|
340
|
-
logger=logger)
|
|
341
|
-
# validate and store the token data
|
|
342
|
-
if token_data:
|
|
343
|
-
user_data: dict[str, Any] = {}
|
|
344
|
-
result = __validate_and_store(iam_server=iam_server,
|
|
345
|
-
user_data=user_data,
|
|
346
|
-
token_data=token_data,
|
|
347
|
-
now=now,
|
|
348
|
-
errors=errors,
|
|
349
|
-
logger=logger)
|
|
350
|
-
else:
|
|
351
|
-
msg: str = "User identification or token not provided"
|
|
352
|
-
if logger:
|
|
353
|
-
logger.error(msg=msg)
|
|
354
|
-
if isinstance(errors, list):
|
|
355
|
-
errors.append(msg)
|
|
356
|
-
|
|
357
|
-
return result
|
|
358
|
-
|
|
359
|
-
|
|
360
28
|
def __request_validate(request: Request) -> Response:
|
|
361
29
|
"""
|
|
362
30
|
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
@@ -412,175 +80,3 @@ def __request_validate(request: Request) -> Response:
|
|
|
412
80
|
result = Response(response="Authorization failed",
|
|
413
81
|
status=401)
|
|
414
82
|
return result
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
def __post_for_token(iam_server: IamServer,
|
|
418
|
-
body_data: dict[str, Any],
|
|
419
|
-
errors: list[str] | None,
|
|
420
|
-
logger: Logger | None) -> dict[str, Any] | None:
|
|
421
|
-
"""
|
|
422
|
-
Send a POST request to obtain the authentication token data, and return the data received.
|
|
423
|
-
|
|
424
|
-
For token acquisition, *body_data* will have the attributes:
|
|
425
|
-
- "grant_type": "authorization_code"
|
|
426
|
-
- "code": <16-character-random-code>
|
|
427
|
-
- "redirect_uri": <redirect-uri>
|
|
428
|
-
|
|
429
|
-
For token refresh, *body_data* will have the attributes:
|
|
430
|
-
- "grant_type": "refresh_token"
|
|
431
|
-
- "refresh_token": <current-refresh-token>
|
|
432
|
-
|
|
433
|
-
For token exchange, *body_data* will have the attributes:
|
|
434
|
-
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
435
|
-
- "subject_token": <token-to-be-exchanged>,
|
|
436
|
-
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
437
|
-
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
438
|
-
- "audience": <client-id>,
|
|
439
|
-
- "subject_issuer": "oidc"
|
|
440
|
-
|
|
441
|
-
These attributes are then added to *body_data*:
|
|
442
|
-
- "client_id": <client-id>,
|
|
443
|
-
- "client_secret": <client-secret>,
|
|
444
|
-
|
|
445
|
-
If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
446
|
-
Otherwise, *errors* will contain the appropriate error message.
|
|
447
|
-
|
|
448
|
-
The typical data set returned contains the following attributes:
|
|
449
|
-
{
|
|
450
|
-
"token_type": "Bearer",
|
|
451
|
-
"access_token": <str>,
|
|
452
|
-
"expires_in": <number-of-seconds>,
|
|
453
|
-
"refresh_token": <str>,
|
|
454
|
-
"refesh_expires_in": <number-of-seconds>
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
:param iam_server: the reference registered *IAM* server
|
|
458
|
-
:param body_data: the data to send in the body of the request
|
|
459
|
-
:param errors: incidental errors
|
|
460
|
-
:param logger: optional logger
|
|
461
|
-
:return: the token data, or *None* if error
|
|
462
|
-
"""
|
|
463
|
-
# initialize the return variable
|
|
464
|
-
result: dict[str, Any] | None = None
|
|
465
|
-
|
|
466
|
-
err_msg: str | None = None
|
|
467
|
-
with _iam_lock:
|
|
468
|
-
# retrieve the IAM server's registry
|
|
469
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
470
|
-
errors=errors,
|
|
471
|
-
logger=logger)
|
|
472
|
-
if registry:
|
|
473
|
-
# complete the data to send in body of request
|
|
474
|
-
body_data["client_id"] = registry["client-id"]
|
|
475
|
-
client_secret: str = registry["client-secret"]
|
|
476
|
-
|
|
477
|
-
# obtain the token
|
|
478
|
-
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
479
|
-
|
|
480
|
-
# log the POST ('client_secret' data must not be shown in log)
|
|
481
|
-
if logger:
|
|
482
|
-
logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
|
|
483
|
-
ensure_ascii=False)}")
|
|
484
|
-
if client_secret:
|
|
485
|
-
body_data["client_secret"] = client_secret
|
|
486
|
-
try:
|
|
487
|
-
# typical return on a token request:
|
|
488
|
-
# {
|
|
489
|
-
# "token_type": "Bearer",
|
|
490
|
-
# "access_token": <str>,
|
|
491
|
-
# "expires_in": <number-of-seconds>,
|
|
492
|
-
# "refresh_token": <str>,
|
|
493
|
-
# "refesh_expires_in": <number-of-seconds>
|
|
494
|
-
# }
|
|
495
|
-
response: requests.Response = requests.post(url=url,
|
|
496
|
-
data=body_data)
|
|
497
|
-
if response.status_code == 200:
|
|
498
|
-
# request succeeded
|
|
499
|
-
result = response.json()
|
|
500
|
-
if logger:
|
|
501
|
-
logger.debug(msg=f"POST success, {json.dumps(obj=result,
|
|
502
|
-
ensure_ascii=False)}")
|
|
503
|
-
else:
|
|
504
|
-
# request resulted in error
|
|
505
|
-
err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
506
|
-
if hasattr(response, "content") and response.content:
|
|
507
|
-
err_msg += f", content '{response.content}'"
|
|
508
|
-
if logger:
|
|
509
|
-
logger.error(msg=err_msg)
|
|
510
|
-
except Exception as e:
|
|
511
|
-
# the operation raised an exception
|
|
512
|
-
err_msg = exc_format(exc=e,
|
|
513
|
-
exc_info=sys.exc_info())
|
|
514
|
-
if logger:
|
|
515
|
-
logger.error(msg=err_msg)
|
|
516
|
-
|
|
517
|
-
if err_msg and isinstance(errors, list):
|
|
518
|
-
errors.append(err_msg)
|
|
519
|
-
|
|
520
|
-
return result
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
def __validate_and_store(iam_server: IamServer,
|
|
524
|
-
user_data: dict[str, Any],
|
|
525
|
-
token_data: dict[str, Any],
|
|
526
|
-
now: int,
|
|
527
|
-
errors: list[str] | None,
|
|
528
|
-
logger: Logger) -> tuple[str, str] | None:
|
|
529
|
-
"""
|
|
530
|
-
Validate and store the token data.
|
|
531
|
-
|
|
532
|
-
The typical *token_data* contains the following attributes:
|
|
533
|
-
{
|
|
534
|
-
"token_type": "Bearer",
|
|
535
|
-
"access_token": <str>,
|
|
536
|
-
"expires_in": <number-of-seconds>,
|
|
537
|
-
"refresh_token": <str>,
|
|
538
|
-
"refesh_expires_in": <number-of-seconds>
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
:param iam_server: the reference registered *IAM* server
|
|
542
|
-
:param user_data: the aurthentication data kepth in *iam_server*'s registry
|
|
543
|
-
:param token_data: the token data
|
|
544
|
-
:param errors: incidental errors
|
|
545
|
-
:param logger: optional logger
|
|
546
|
-
:return: tuple containing the user identification and the validated and stored token, or *None* if error
|
|
547
|
-
"""
|
|
548
|
-
# initialize the return variable
|
|
549
|
-
result: tuple[str, str] | None = None
|
|
550
|
-
|
|
551
|
-
with _iam_lock:
|
|
552
|
-
# retrieve the IAM server's registry
|
|
553
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
554
|
-
errors=errors,
|
|
555
|
-
logger=logger)
|
|
556
|
-
if registry:
|
|
557
|
-
token: str = token_data.get("access_token")
|
|
558
|
-
user_data["access-token"] = token
|
|
559
|
-
# keep current refresh token if a new one is not provided
|
|
560
|
-
if token_data.get("refresh_token"):
|
|
561
|
-
user_data["refresh-token"] = token_data.get("refresh_token")
|
|
562
|
-
user_data["access-expiration"] = now + token_data.get("expires_in")
|
|
563
|
-
refresh_exp: int = user_data.get("refresh_expires_in")
|
|
564
|
-
user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
|
|
565
|
-
# public_key: str = _get_public_key(iam_server=iam_server,
|
|
566
|
-
# errors=errors,
|
|
567
|
-
# logger=logger)
|
|
568
|
-
recipient_attr = registry["recipient-attr"]
|
|
569
|
-
login_id = user_data.pop("login-id", None)
|
|
570
|
-
claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
571
|
-
issuer=registry["base-url"],
|
|
572
|
-
recipient_id=login_id,
|
|
573
|
-
recipient_attr=recipient_attr,
|
|
574
|
-
# public_key=public_key,
|
|
575
|
-
errors=errors,
|
|
576
|
-
logger=logger)
|
|
577
|
-
if claims:
|
|
578
|
-
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
579
|
-
errors=errors,
|
|
580
|
-
logger=logger)
|
|
581
|
-
# must test with 'not errors'
|
|
582
|
-
if not errors:
|
|
583
|
-
user_id: str = login_id if login_id else claims["payload"][recipient_attr]
|
|
584
|
-
users[user_id] = user_data
|
|
585
|
-
result = (user_id, token)
|
|
586
|
-
return result
|
pypomes_iam/iam_services.py
CHANGED
|
@@ -4,9 +4,9 @@ from logging import Logger
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from .iam_common import IamServer, _iam_lock, _iam_server_from_endpoint
|
|
7
|
-
from .
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
from .iam_actions import (
|
|
8
|
+
action_login, action_logout,
|
|
9
|
+
action_token, action_exchange, action_callback
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
# the logger for IAM service operations
|
|
@@ -60,10 +60,10 @@ def service_login() -> Response:
|
|
|
60
60
|
logger=__IAM_LOGGER)
|
|
61
61
|
if iam_server:
|
|
62
62
|
# obtain the login URL
|
|
63
|
-
login_url: str =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
login_url: str = action_login(iam_server=iam_server,
|
|
64
|
+
args=request.args,
|
|
65
|
+
errors=errors,
|
|
66
|
+
logger=__IAM_LOGGER)
|
|
67
67
|
if login_url:
|
|
68
68
|
result = jsonify({"login-url": login_url})
|
|
69
69
|
if errors:
|
|
@@ -106,10 +106,10 @@ def service_logout() -> Response:
|
|
|
106
106
|
logger=__IAM_LOGGER)
|
|
107
107
|
if iam_server:
|
|
108
108
|
# logout the user
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
action_logout(iam_server=iam_server,
|
|
110
|
+
args=request.args,
|
|
111
|
+
errors=errors,
|
|
112
|
+
logger=__IAM_LOGGER)
|
|
113
113
|
if errors:
|
|
114
114
|
result = Response(response="; ".join(errors),
|
|
115
115
|
status=400)
|
|
@@ -160,10 +160,10 @@ def service_callback() -> Response:
|
|
|
160
160
|
logger=__IAM_LOGGER)
|
|
161
161
|
if iam_server:
|
|
162
162
|
# process the callback operation
|
|
163
|
-
token_data =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
token_data = action_callback(iam_server=iam_server,
|
|
164
|
+
args=request.args,
|
|
165
|
+
errors=errors,
|
|
166
|
+
logger=__IAM_LOGGER)
|
|
167
167
|
result: Response
|
|
168
168
|
if errors:
|
|
169
169
|
result = jsonify({"errors": "; ".join(errors)})
|
|
@@ -215,10 +215,10 @@ def service_token() -> Response:
|
|
|
215
215
|
if iam_server:
|
|
216
216
|
# retrieve the token
|
|
217
217
|
errors: list[str] = []
|
|
218
|
-
token: str =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
token: str = action_token(iam_server=iam_server,
|
|
219
|
+
args=args,
|
|
220
|
+
errors=errors,
|
|
221
|
+
logger=__IAM_LOGGER)
|
|
222
222
|
else:
|
|
223
223
|
msg: str = "User identification not provided"
|
|
224
224
|
errors.append(msg)
|
|
@@ -278,10 +278,10 @@ def service_exchange() -> Response:
|
|
|
278
278
|
token_data: dict[str, Any] | None = None
|
|
279
279
|
if iam_server:
|
|
280
280
|
errors: list[str] = []
|
|
281
|
-
token_data =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
token_data = action_exchange(iam_server=iam_server,
|
|
282
|
+
args=request.args,
|
|
283
|
+
errors=errors,
|
|
284
|
+
logger=__IAM_LOGGER)
|
|
285
285
|
result: Response
|
|
286
286
|
if errors:
|
|
287
287
|
result = Response(response="; ".join(errors),
|
pypomes_iam/jusbr_pomes.py
CHANGED
|
@@ -7,7 +7,7 @@ from pypomes_core import (
|
|
|
7
7
|
from typing import Any, Final
|
|
8
8
|
|
|
9
9
|
from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
|
|
10
|
-
from .
|
|
10
|
+
from .iam_actions import action_token
|
|
11
11
|
|
|
12
12
|
JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
|
|
13
13
|
JUSBR_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_SECRET")
|
|
@@ -118,8 +118,8 @@ def jusbr_get_token(user_id: str,
|
|
|
118
118
|
# retrieve the token
|
|
119
119
|
args: dict[str, Any] = {"user-id": user_id}
|
|
120
120
|
with _iam_lock:
|
|
121
|
-
result =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
result = action_token(iam_server=IamServer.IAM_JUSRBR,
|
|
122
|
+
args=args,
|
|
123
|
+
errors=errors,
|
|
124
|
+
logger=logger)
|
|
125
125
|
return result
|
pypomes_iam/keycloak_pomes.py
CHANGED
|
@@ -7,7 +7,7 @@ from pypomes_core import (
|
|
|
7
7
|
from typing import Any, Final
|
|
8
8
|
|
|
9
9
|
from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
|
|
10
|
-
from .
|
|
10
|
+
from .iam_actions import action_token
|
|
11
11
|
|
|
12
12
|
KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
|
|
13
13
|
KEYCLOAK_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_SECRET")
|
|
@@ -129,8 +129,8 @@ def keycloak_get_token(user_id: str,
|
|
|
129
129
|
# retrieve the token
|
|
130
130
|
args: dict[str, Any] = {"user-id": user_id}
|
|
131
131
|
with _iam_lock:
|
|
132
|
-
result =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
result = action_token(iam_server=IamServer.IAM_KEYCLOAK,
|
|
133
|
+
args=args,
|
|
134
|
+
errors=errors,
|
|
135
|
+
logger=logger)
|
|
136
136
|
return result
|
pypomes_iam/provider_pomes.py
CHANGED
|
@@ -5,7 +5,7 @@ from base64 import b64encode
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from logging import Logger
|
|
7
7
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
8
|
-
from threading import
|
|
8
|
+
from threading import Lock
|
|
9
9
|
from typing import Any, Final
|
|
10
10
|
|
|
11
11
|
# structure:
|
|
@@ -25,7 +25,7 @@ _provider_registry: Final[dict[str, dict[str, Any]]] = {}
|
|
|
25
25
|
|
|
26
26
|
# the lock protecting the data in '_provider_registry'
|
|
27
27
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
28
|
-
_provider_lock: Final[
|
|
28
|
+
_provider_lock: Final[Lock] = Lock()
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def provider_register(provider_id: str,
|
|
@@ -133,7 +133,7 @@ def provider_get_token(provider_id: str,
|
|
|
133
133
|
provider["access-token"] = reply.get("access_token")
|
|
134
134
|
provider["access-expiration"] = now + int(reply.get("expires_in"))
|
|
135
135
|
if reply.get("refresh_token"):
|
|
136
|
-
provider["refresh-token"] = reply["
|
|
136
|
+
provider["refresh-token"] = reply["refresh_token"]
|
|
137
137
|
if reply.get("refresh_expires_in"):
|
|
138
138
|
provider["refresh-expiration"] = now + int(reply.get("refresh_expires_in"))
|
|
139
139
|
else:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
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,13 @@
|
|
|
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,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pypomes_iam/__init__.py,sha256=KX_QLdqAD-dNUl3G1mDeutxL9e58S9OsMoJlrgM9R28,1027
|
|
2
|
-
pypomes_iam/iam_common.py,sha256=xRMVPIDxbVPoudPDKFCqGCA7Klt9HsZM61dSGfEy7Tw,10364
|
|
3
|
-
pypomes_iam/iam_pomes.py,sha256=sB2DDCaN5nN6ehZH4HCsWxPM9IbP5IRt42UhIRtn07Q,27436
|
|
4
|
-
pypomes_iam/iam_services.py,sha256=fdpOc6EmCLcMtkhZU5OX9gYJIA1RNyM3JcoMX0RqwXA,11829
|
|
5
|
-
pypomes_iam/jusbr_pomes.py,sha256=cuQWB5OTeAHarmUqAGIU4udSEJA1C6W6lOtauWA7gqw,5904
|
|
6
|
-
pypomes_iam/keycloak_pomes.py,sha256=OvPhfUXpqxll-p6CdRq2j5jp5ST9Z0feXg6TVbqC2cY,6751
|
|
7
|
-
pypomes_iam/provider_pomes.py,sha256=1Jki3hHBt5SRQB8f9MU7hQ-WApJ5LCuj7QRJFdrALbw,7235
|
|
8
|
-
pypomes_iam/token_pomes.py,sha256=Bz9pT2oU6jTEr_ZEZEJ3kUjH3TfxRyY1_vR319v6CEo,6692
|
|
9
|
-
pypomes_iam-0.5.4.dist-info/METADATA,sha256=rMdeh8lld9QGoNKpzFF0Uc-x1OuUYhn3TkDY3Nzk1NQ,694
|
|
10
|
-
pypomes_iam-0.5.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
-
pypomes_iam-0.5.4.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
12
|
-
pypomes_iam-0.5.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|