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/iam_pomes.py CHANGED
@@ -1,586 +1,156 @@
1
- import json
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 TZ_LOCAL, exc_format
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
- _get_iam_users, _get_iam_registry, # _get_public_key,
15
- _get_login_timeout, _get_user_data, _iam_server_from_issuer
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
- err_msg: str | None = None
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
- # 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>
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
- :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
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: tuple containing the user identification and the validated and stored token, or *None* if error
144
+ :return: the uthentication tokem
547
145
  """
548
- # initialize the return variable
549
- result: tuple[str, str] | None = None
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
- # 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)
152
+ result = action_token(iam_server=iam_server,
153
+ args=args,
154
+ errors=errors,
155
+ logger=logger)
586
156
  return result