pypomes-iam 0.5.2__py3-none-any.whl → 0.6.2__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 +16 -18
- pypomes_iam/iam_actions.py +822 -0
- pypomes_iam/iam_common.py +170 -65
- pypomes_iam/iam_pomes.py +140 -570
- pypomes_iam/iam_services.py +113 -28
- pypomes_iam/provider_pomes.py +160 -79
- pypomes_iam/token_pomes.py +0 -2
- {pypomes_iam-0.5.2.dist-info → pypomes_iam-0.6.2.dist-info}/METADATA +1 -2
- pypomes_iam-0.6.2.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -125
- pypomes_iam/keycloak_pomes.py +0 -136
- pypomes_iam-0.5.2.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.2.dist-info → pypomes_iam-0.6.2.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.2.dist-info → pypomes_iam-0.6.2.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_pomes.py
CHANGED
|
@@ -1,586 +1,156 @@
|
|
|
1
|
-
import
|
|
2
|
-
import requests
|
|
3
|
-
import secrets
|
|
4
|
-
import string
|
|
5
|
-
import sys
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from flask import Request, Response, request
|
|
1
|
+
from flask import Flask
|
|
8
2
|
from logging import Logger
|
|
9
|
-
from pypomes_core import
|
|
3
|
+
from pypomes_core import APP_PREFIX, env_get_int, env_get_str
|
|
10
4
|
from typing import Any
|
|
11
5
|
|
|
12
6
|
from .iam_common import (
|
|
13
|
-
IamServer, _iam_lock
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
_IAM_SERVERS, IamServer, IamParam, _iam_lock
|
|
8
|
+
)
|
|
9
|
+
from .iam_actions import action_token
|
|
10
|
+
from .iam_services import (
|
|
11
|
+
service_login, service_logout, service_callback, service_exchange, service_token
|
|
16
12
|
)
|
|
17
|
-
from .token_pomes import token_get_claims, token_validate
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def jwt_required(func: callable) -> callable:
|
|
21
|
-
"""
|
|
22
|
-
Create a decorator to authenticate service endpoints with JWT tokens.
|
|
23
|
-
|
|
24
|
-
:param func: the function being decorated
|
|
25
|
-
"""
|
|
26
|
-
# ruff: noqa: ANN003 - Missing type annotation for *{name}
|
|
27
|
-
def wrapper(*args, **kwargs) -> Response:
|
|
28
|
-
response: Response = __request_validate(request=request)
|
|
29
|
-
return response if response else func(*args, **kwargs)
|
|
30
|
-
|
|
31
|
-
# prevent a rogue error ("View function mapping is overwriting an existing endpoint function")
|
|
32
|
-
wrapper.__name__ = func.__name__
|
|
33
|
-
|
|
34
|
-
return wrapper
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
def __request_validate(request: Request) -> Response:
|
|
361
|
-
"""
|
|
362
|
-
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
363
|
-
|
|
364
|
-
This implementation assumes that HTTP requests are handled with the *Flask* framework.
|
|
365
|
-
|
|
366
|
-
:param request: the *request* to be verified
|
|
367
|
-
:return: *None* if the *request* is valid, otherwise a *Response* reporting the error
|
|
368
|
-
"""
|
|
369
|
-
# initialize the return variable
|
|
370
|
-
result: Response | None = None
|
|
371
|
-
|
|
372
|
-
# retrieve the authorization from the request header
|
|
373
|
-
auth_header: str = request.headers.get("Authorization")
|
|
374
|
-
|
|
375
|
-
# validate the authorization token
|
|
376
|
-
bad_token: bool = True
|
|
377
|
-
if auth_header and auth_header.startswith("Bearer "):
|
|
378
|
-
# extract and validate the JWT access token
|
|
379
|
-
token: str = auth_header.split(" ")[1]
|
|
380
|
-
claims: dict[str, Any] = token_get_claims(token=token)
|
|
381
|
-
if claims:
|
|
382
|
-
issuer: str = claims["payload"].get("iss")
|
|
383
|
-
recipient_attr: str | None = None
|
|
384
|
-
recipient_id: str = request.values.get("user-id") or request.values.get("login")
|
|
385
|
-
with _iam_lock:
|
|
386
|
-
iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
|
|
387
|
-
errors=None,
|
|
388
|
-
logger=None)
|
|
389
|
-
# public_key: str = _get_public_key(iam_server=iam_server,
|
|
390
|
-
# errors=errors,
|
|
391
|
-
# logger=logger)
|
|
392
|
-
public_key = None
|
|
393
|
-
|
|
394
|
-
# validate the token's recipient only if a user identification is provided
|
|
395
|
-
if recipient_id:
|
|
396
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
397
|
-
errors=None,
|
|
398
|
-
logger=None)
|
|
399
|
-
recipient_attr = registry["recipient-attr"]
|
|
400
|
-
|
|
401
|
-
# validate the token
|
|
402
|
-
if token_validate(token=token,
|
|
403
|
-
issuer=issuer,
|
|
404
|
-
recipient_id=recipient_id,
|
|
405
|
-
recipient_attr=recipient_attr,
|
|
406
|
-
public_key=public_key):
|
|
407
|
-
# token is valid
|
|
408
|
-
bad_token = False
|
|
409
|
-
|
|
410
|
-
# deny the authorization
|
|
411
|
-
if bad_token:
|
|
412
|
-
result = Response(response="Authorization failed",
|
|
413
|
-
status=401)
|
|
414
|
-
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
13
|
|
|
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
14
|
|
|
466
|
-
|
|
15
|
+
def iam_setup(flask_app: Flask,
|
|
16
|
+
iam_server: IamServer,
|
|
17
|
+
base_url: str,
|
|
18
|
+
client_id: str,
|
|
19
|
+
client_realm: str,
|
|
20
|
+
client_secret: str | None,
|
|
21
|
+
recipient_attribute: str,
|
|
22
|
+
admin_id: str = None,
|
|
23
|
+
admin_secret: str = None,
|
|
24
|
+
login_timeout: int = None,
|
|
25
|
+
public_key_lifetime: int = None,
|
|
26
|
+
callback_endpoint: str = None,
|
|
27
|
+
exchange_endpoint: str = None,
|
|
28
|
+
login_endpoint: str = None,
|
|
29
|
+
logout_endpoint: str = None,
|
|
30
|
+
token_endpoint: str = None) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Establish the provided parameters for configuring the *IAM* server *iam_server*.
|
|
33
|
+
|
|
34
|
+
The parameters *admin_id* and *admin_* are required only if administrative are task are planned.
|
|
35
|
+
The optional parameter *client_timeout* refers to the maximum time in seconds allowed for the
|
|
36
|
+
user to login at the *IAM* server's login page, and defaults to no time limit.
|
|
37
|
+
|
|
38
|
+
The parameter *client_secret* is required in most requests to the *IAM* server. In the case
|
|
39
|
+
it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
|
|
40
|
+
the first time it is needed.
|
|
41
|
+
|
|
42
|
+
:param flask_app: the Flask application
|
|
43
|
+
:param iam_server: identifies the supported *IAM* server (*jusbr* or *keycloak*)
|
|
44
|
+
:param base_url: base URL to request services
|
|
45
|
+
:param client_id: the client's identification with the *IAM* server
|
|
46
|
+
:param client_realm: the client realm
|
|
47
|
+
:param client_secret: the client's password with the *IAM* server
|
|
48
|
+
:param recipient_attribute: attribute in the token's payload holding the token's subject
|
|
49
|
+
:param admin_id: identifies the realm administrator
|
|
50
|
+
:param admin_secret: password for the realm administrator
|
|
51
|
+
:param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
|
|
52
|
+
:param public_key_lifetime: how long to use *IAM* server's public key, before refreshing it (in seconds)
|
|
53
|
+
:param callback_endpoint: endpoint for the callback from the front end
|
|
54
|
+
:param exchange_endpoint: endpoint for requesting token exchange
|
|
55
|
+
:param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
|
|
56
|
+
:param logout_endpoint: endpoint for terminating user access
|
|
57
|
+
:param token_endpoint: endpoint for retrieving authentication token
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# configure the Keycloak registry
|
|
467
61
|
with _iam_lock:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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>
|
|
62
|
+
_IAM_SERVERS[iam_server] = {
|
|
63
|
+
IamParam.URL_BASE: base_url,
|
|
64
|
+
IamParam.CLIENT_ID: client_id,
|
|
65
|
+
IamParam.CLIENT_REALM: client_realm,
|
|
66
|
+
IamParam.CLIENT_SECRET: client_secret,
|
|
67
|
+
IamParam.RECIPIENT_ATTR: recipient_attribute,
|
|
68
|
+
IamParam.ADMIN_ID: admin_id,
|
|
69
|
+
IamParam.ADMIN_SECRET: admin_secret,
|
|
70
|
+
IamParam.LOGIN_TIMEOUT: login_timeout,
|
|
71
|
+
IamParam.PK_LIFETIME: public_key_lifetime,
|
|
72
|
+
IamParam.PK_EXPIRATION: 0,
|
|
73
|
+
IamParam.PUBLIC_KEY: None,
|
|
74
|
+
IamParam.USERS: {}
|
|
539
75
|
}
|
|
540
76
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
77
|
+
# establish the endpoints
|
|
78
|
+
if callback_endpoint:
|
|
79
|
+
flask_app.add_url_rule(rule=callback_endpoint,
|
|
80
|
+
endpoint=f"{iam_server}-callback",
|
|
81
|
+
view_func=service_callback,
|
|
82
|
+
methods=["GET"])
|
|
83
|
+
if login_endpoint:
|
|
84
|
+
flask_app.add_url_rule(rule=login_endpoint,
|
|
85
|
+
endpoint=f"{iam_server}-login",
|
|
86
|
+
view_func=service_login,
|
|
87
|
+
methods=["GET"])
|
|
88
|
+
if logout_endpoint:
|
|
89
|
+
flask_app.add_url_rule(rule=logout_endpoint,
|
|
90
|
+
endpoint=f"{iam_server}-logout",
|
|
91
|
+
view_func=service_logout,
|
|
92
|
+
methods=["GET"])
|
|
93
|
+
if token_endpoint:
|
|
94
|
+
flask_app.add_url_rule(rule=token_endpoint,
|
|
95
|
+
endpoint=f"{iam_server}-token",
|
|
96
|
+
view_func=service_token,
|
|
97
|
+
methods=["GET"])
|
|
98
|
+
if exchange_endpoint:
|
|
99
|
+
flask_app.add_url_rule(rule=exchange_endpoint,
|
|
100
|
+
endpoint=f"{iam_server}-exchange",
|
|
101
|
+
view_func=service_exchange,
|
|
102
|
+
methods=["POST"])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def iam_get_env_parameters(iam_prefix: str = None) -> dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Retrieve the set parameters for a *IAM* server from the environment.
|
|
108
|
+
|
|
109
|
+
the parameters are returned ready to be used as a '**kwargs' parameter set in a call to *iam_setup()*,
|
|
110
|
+
and sorted in the order appropriate to use them instead with a '*args' parameter set.
|
|
111
|
+
|
|
112
|
+
:param iam_prefix: the prefix classifying the parameters
|
|
113
|
+
:return: the sorted parameters classified by *prefix*
|
|
114
|
+
"""
|
|
115
|
+
return {
|
|
116
|
+
"base_url": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_URL_AUTH_BASE"),
|
|
117
|
+
"client_id": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_ID"),
|
|
118
|
+
"client_realm": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_REALM"),
|
|
119
|
+
"client_secret": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_SECRET"),
|
|
120
|
+
"recipient_attribute": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_RECIPIENT_ATTR"),
|
|
121
|
+
"admin_id": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ADMIN_ID"),
|
|
122
|
+
"admin_secret": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ADMIN_SECRET"),
|
|
123
|
+
"login_timeout": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_LOGIN_TIMEOUT"),
|
|
124
|
+
"public_key_lifetime": env_get_int(key=f"{APP_PREFIX}_{iam_prefix}_PUBLIC_KEY_LIFETIME"),
|
|
125
|
+
"callback_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_CALLBACK"),
|
|
126
|
+
"exchange_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_EXCHANGE"),
|
|
127
|
+
"login_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_LOGIN"),
|
|
128
|
+
"logout_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_LOGOUT"),
|
|
129
|
+
"token_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_TOKEN")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def iam_get_token(iam_server: IamServer,
|
|
134
|
+
user_id: str,
|
|
135
|
+
errors: list[str] = None,
|
|
136
|
+
logger: Logger = None) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Retrieve an authentication token for *user_id*.
|
|
139
|
+
|
|
140
|
+
:param iam_server: identifies the *IAM* server
|
|
141
|
+
:param user_id: identifies the user
|
|
544
142
|
:param errors: incidental errors
|
|
545
143
|
:param logger: optional logger
|
|
546
|
-
:return:
|
|
144
|
+
:return: the uthentication tokem
|
|
547
145
|
"""
|
|
548
|
-
#
|
|
549
|
-
result:
|
|
146
|
+
# declare the return variable
|
|
147
|
+
result: str
|
|
550
148
|
|
|
149
|
+
# retrieve the token
|
|
150
|
+
args: dict[str, Any] = {"user-id": user_id}
|
|
551
151
|
with _iam_lock:
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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)
|
|
152
|
+
result = action_token(iam_server=iam_server,
|
|
153
|
+
args=args,
|
|
154
|
+
errors=errors,
|
|
155
|
+
logger=logger)
|
|
586
156
|
return result
|