pypomes-iam 0.3.5__tar.gz → 0.3.7__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.3.5
3
+ Version: 0.3.7
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
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.3.5"
9
+ version = "0.3.7"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -5,6 +5,7 @@ from enum import StrEnum
5
5
  from logging import Logger
6
6
  from pypomes_core import TZ_LOCAL, exc_format
7
7
  from pypomes_crypto import crypto_jwk_convert
8
+ from threading import Lock
8
9
  from typing import Any, Final
9
10
 
10
11
 
@@ -16,7 +17,8 @@ class IamServer(StrEnum):
16
17
  IAM_KEYCLOAK = "iam-keycloak"
17
18
 
18
19
 
19
- # the logger for IAM operations
20
+ # the logger for IAM service operations
21
+ # (used exclusively at the HTTP endpoint - all other functions receive the lgger as parameter)
20
22
  __IAM_LOGGER: Logger | None = None
21
23
 
22
24
  # registry structure:
@@ -51,11 +53,18 @@ __IAM_LOGGER: Logger | None = None
51
53
  # }
52
54
  _IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
53
55
 
56
+ # the lock protecting the data in '_IAM_SERVER'
57
+ # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
58
+ _iam_lock: Final[Lock] = Lock()
59
+
54
60
 
55
61
  def _get_logger() -> Logger | None:
56
62
  """
57
63
  Retrieve the registered logger for *IAM* operations.
58
64
 
65
+ This function is invoked exclusively from the HTTP endpoints.
66
+ All other functions receive the logger as parameter.
67
+
59
68
  :return: the registered logger for *IAM* operations.
60
69
  """
61
70
  return __IAM_LOGGER
@@ -3,14 +3,13 @@ import requests
3
3
  import secrets
4
4
  import string
5
5
  import sys
6
- from cachetools import Cache
7
6
  from datetime import datetime
8
7
  from logging import Logger
9
8
  from pypomes_core import TZ_LOCAL, exc_format
10
9
  from typing import Any
11
10
 
12
11
  from .iam_common import (
13
- IamServer,
12
+ IamServer, _iam_lock,
14
13
  _register_logger, _get_iam_users, _get_iam_registry,
15
14
  _get_login_timeout, _get_user_data, _get_public_key
16
15
  )
@@ -48,30 +47,34 @@ def user_login(iam_server: IamServer,
48
47
  # build the user data
49
48
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
50
49
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
51
- user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
52
- user_id=oauth_state,
53
- errors=errors,
54
- logger=logger)
55
- if user_data:
56
- user_data["login-id"] = user_id
57
- timeout: int = _get_login_timeout(iam_server=iam_server,
58
- errors=errors,
59
- logger=logger)
60
- if not errors:
61
- user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
62
- redirect_uri: str = args.get("redirect-uri")
63
-
64
- # build the login url
65
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
66
- errors=errors,
67
- logger=logger)
68
- if registry:
69
- registry["redirect-uri"] = redirect_uri
70
- result = {"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
71
- f"?response_type=code&scope=openid"
72
- f"&client_id={registry["client-id"]}"
73
- f"&redirect_uri={redirect_uri}"
74
- f"&state={oauth_state}")}
50
+
51
+ with _iam_lock:
52
+ # retrieve the user data from the IAM server's registry
53
+ user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
54
+ user_id=oauth_state,
55
+ errors=errors,
56
+ logger=logger)
57
+ if user_data:
58
+ user_data["login-id"] = user_id
59
+ timeout: int = _get_login_timeout(iam_server=iam_server,
60
+ errors=errors,
61
+ logger=logger)
62
+ if not errors:
63
+ user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
64
+ if timeout else None
65
+ redirect_uri: str = args.get("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
+ registry["redirect-uri"] = redirect_uri
73
+ result = {"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
74
+ f"?response_type=code&scope=openid"
75
+ f"&client_id={registry["client-id"]}"
76
+ f"&redirect_uri={redirect_uri}"
77
+ f"&state={oauth_state}")}
75
78
  return result
76
79
 
77
80
 
@@ -94,14 +97,15 @@ def user_logout(iam_server: IamServer,
94
97
  user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
95
98
 
96
99
  if user_id:
97
- # retrieve the IAM server's cache storage
98
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
99
- errors=errors,
100
- logger=logger) or {}
101
- if user_id in users:
102
- users.pop(user_id)
103
- if logger:
104
- logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
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")
105
109
 
106
110
 
107
111
  def user_token(iam_server: IamServer,
@@ -111,7 +115,7 @@ def user_token(iam_server: IamServer,
111
115
  """
112
116
  Retrieve the authentication token for the user, from *iam_server*.
113
117
 
114
- The user is identified by the attribute *user-id*, *user_id*, or "login", provided in *args*.
118
+ The user is identified by the attribute *user-id*, *user_id*, or *login*, provided in *args*.
115
119
 
116
120
  :param iam_server: the reference registered *IAM* server
117
121
  :param args: the arguments passed when requesting the service
@@ -127,56 +131,58 @@ def user_token(iam_server: IamServer,
127
131
 
128
132
  err_msg: str | None = None
129
133
  if user_id:
130
- user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
131
- user_id=user_id,
132
- errors=errors,
133
- logger=logger)
134
- token: str = user_data["access-token"] if user_data else None
135
- if token:
136
- access_expiration: int = user_data.get("access-expiration")
137
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
138
- if now < access_expiration:
139
- result = token
140
- else:
141
- # access token has expired
142
- refresh_token: str = user_data["refresh-token"]
143
- if refresh_token:
144
- refresh_expiration = user_data["refresh-expiration"]
145
- if now < refresh_expiration:
146
- body_data: dict[str, str] = {
147
- "grant_type": "refresh_token",
148
- "refresh_token": refresh_token
149
- }
150
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
151
- token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
152
- body_data=body_data,
153
- errors=errors,
154
- logger=logger)
155
- # validate and store the token data
156
- if token_data:
157
- token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
158
- user_data=user_data,
159
- token_data=token_data,
160
- now=now,
161
- errors=errors,
162
- logger=logger)
163
- result = token_info[1]
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
164
173
  else:
165
- # refresh token is no longer valid
166
- user_data["refresh-token"] = None
174
+ # refresh token has expired
175
+ err_msg = "Access and refresh tokens expired"
176
+ if logger:
177
+ logger.error(msg=err_msg)
167
178
  else:
168
- # refresh token has expired
169
- err_msg = "Access and refresh tokens expired"
179
+ err_msg = "Access token expired, no refresh token available"
170
180
  if logger:
171
181
  logger.error(msg=err_msg)
172
- else:
173
- err_msg = "Access token expired, no refresh token available"
174
- if logger:
175
- logger.error(msg=err_msg)
176
- else:
177
- err_msg = f"User '{user_id}' not authenticated"
178
- if logger:
179
- 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)
180
186
  else:
181
187
  err_msg = "User identification not provided"
182
188
  if logger:
@@ -193,7 +199,11 @@ def login_callback(iam_server: IamServer,
193
199
  errors: list[str] = None,
194
200
  logger: Logger = None) -> tuple[str, str] | None:
195
201
  """
196
- Entry point for the callback from *iam_server* via the front-end application, on authentication operation.
202
+ Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
203
+
204
+ The relevant arguments received are:
205
+ - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
206
+ - *code*: the temporary authorization code, to be exchanged for the token
197
207
 
198
208
  :param iam_server: the reference registered *IAM* server
199
209
  :param args: the arguments passed when requesting the service
@@ -204,15 +214,13 @@ def login_callback(iam_server: IamServer,
204
214
  # initialize the return variable
205
215
  result: tuple[str, str] | None = None
206
216
 
207
- # retrieve the users authentication data
208
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
209
- errors=errors,
210
- logger=logger)
211
- cache: Cache = registry["cache"] if registry else None
212
- if cache:
213
- users: dict[str, dict[str, Any]] = cache.get("users")
214
-
215
- # validate the OAuth2 state
217
+ with _iam_lock:
218
+ # retrieve the IAM server's registry and the data for all users therein
219
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
220
+ errors=errors,
221
+ logger=logger)
222
+ users: dict[str, dict[str, Any]] = (registry["cache"]["users"] or {}) if registry else {}
223
+ # retrieve the OAuth2 state
216
224
  oauth_state: str = args.get("state")
217
225
  user_data: dict[str, Any] | None = None
218
226
  if oauth_state:
@@ -221,7 +229,7 @@ def login_callback(iam_server: IamServer,
221
229
  user_data = data
222
230
  break
223
231
 
224
- # exchange 'code' for the token
232
+ # exchange 'code' received for the token
225
233
  if user_data:
226
234
  expiration: int = user_data["login-expiration"] or sys.maxsize
227
235
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
@@ -268,6 +276,15 @@ def token_exchange(iam_server: IamServer,
268
276
  - client-id: identification for the reference user (aliases: 'client_id', 'login')
269
277
  - token: the token to be exchanged
270
278
 
279
+ The typical data set returned contains the following attributes:
280
+ {
281
+ "token_type": "Bearer",
282
+ "access_token": <str>,
283
+ "expires_in": <number-of-seconds>,
284
+ "refresh_token": <str>,
285
+ "refesh_expires_in": <number-of-seconds>
286
+ }
287
+
271
288
  :param iam_server: the reference registered *IAM* server
272
289
  :param args: the arguments passed when requesting the service
273
290
  :param errors: incidental errors
@@ -280,40 +297,41 @@ def token_exchange(iam_server: IamServer,
280
297
  # obtain the user's identification
281
298
  user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
282
299
 
283
- # retrieve the token to be exchanges
300
+ # obtain the token to be exchanges
284
301
  token: str = args.get("token")
285
302
 
286
303
  if user_id and token:
287
304
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
288
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
289
- errors=errors,
290
- logger=logger)
291
- if registry:
292
- body_data: dict[str, str] = {
293
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
294
- "subject_token": token,
295
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
296
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
297
- "audience": registry["client-id"],
298
- "subject_issuer": "oidc"
299
- }
300
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
301
- token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
302
- body_data=body_data,
303
- errors=errors,
304
- logger=logger)
305
- # validate and store the token data
306
- if token_data:
307
- user_data: dict[str, Any] = {}
308
- result = __validate_and_store(iam_server=iam_server,
309
- user_data=user_data,
310
- token_data=token_data,
311
- now=now,
312
- errors=errors,
313
- logger=logger)
314
-
305
+ with _iam_lock:
306
+ # retrieve the IAM server's registry
307
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
308
+ errors=errors,
309
+ logger=logger)
310
+ if registry:
311
+ body_data: dict[str, str] = {
312
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
313
+ "subject_token": token,
314
+ "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
315
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
316
+ "audience": registry["client-id"],
317
+ "subject_issuer": "oidc"
318
+ }
319
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
320
+ token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
321
+ body_data=body_data,
322
+ errors=errors,
323
+ logger=logger)
324
+ # validate and store the token data
325
+ if token_data:
326
+ user_data: dict[str, Any] = {}
327
+ result = __validate_and_store(iam_server=iam_server,
328
+ user_data=user_data,
329
+ token_data=token_data,
330
+ now=now,
331
+ errors=errors,
332
+ logger=logger)
315
333
  else:
316
- msg: str = "User identification and token must be provided"
334
+ msg: str = "User identification or token not provided"
317
335
  if logger:
318
336
  logger.error(msg=msg)
319
337
  if isinstance(errors, list):
@@ -353,7 +371,7 @@ def __post_for_token(iam_server: IamServer,
353
371
  If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
354
372
  Otherwise, *errors* will contain the appropriate error message.
355
373
 
356
- The typical data returned contains the following attributes:
374
+ The typical data set returned contains the following attributes:
357
375
  {
358
376
  "token_type": "Bearer",
359
377
  "access_token": <str>,
@@ -371,52 +389,53 @@ def __post_for_token(iam_server: IamServer,
371
389
  # initialize the return variable
372
390
  result: dict[str, Any] | None = None
373
391
 
374
- # PBTAIN THE iam SERVER'S REGISTRY
375
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
376
- errors=errors,
377
- logger=logger)
378
392
  err_msg: str | None = None
379
- if registry:
380
- # complete the data to send in body of request
381
- body_data["client_id"] = registry["client-id"]
382
- client_secret: str = registry["client-secret"]
383
- if client_secret:
384
- body_data["client_secret"] = client_secret
385
-
386
- # obtain the token
387
- url: str = registry["base-url"] + "/protocol/openid-connect/token"
388
- if logger:
389
- logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
390
- ensure_ascii=False)}")
391
- try:
392
- # typical return on a token request:
393
- # {
394
- # "token_type": "Bearer",
395
- # "access_token": <str>,
396
- # "expires_in": <number-of-seconds>,
397
- # "refresh_token": <str>,
398
- # "refesh_expires_in": <number-of-seconds>
399
- # }
400
- response: requests.Response = requests.post(url=url,
401
- data=body_data)
402
- if response.status_code == 200:
403
- # request succeeded
404
- if logger:
405
- logger.debug(msg=f"POST success, status {response.status_code}")
406
- result = response.json()
407
- else:
408
- # request resulted in error
409
- err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
410
- if hasattr(response, "content") and response.content:
411
- err_msg += f", content '{response.content}'"
393
+ with _iam_lock:
394
+ # retrieve the IAM server's registry
395
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
396
+ errors=errors,
397
+ logger=logger)
398
+ if registry:
399
+ # complete the data to send in body of request
400
+ body_data["client_id"] = registry["client-id"]
401
+ client_secret: str = registry["client-secret"]
402
+ if client_secret:
403
+ body_data["client_secret"] = client_secret
404
+
405
+ # obtain the token
406
+ url: str = registry["base-url"] + "/protocol/openid-connect/token"
407
+ if logger:
408
+ logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
409
+ ensure_ascii=False)}")
410
+ try:
411
+ # typical return on a token request:
412
+ # {
413
+ # "token_type": "Bearer",
414
+ # "access_token": <str>,
415
+ # "expires_in": <number-of-seconds>,
416
+ # "refresh_token": <str>,
417
+ # "refesh_expires_in": <number-of-seconds>
418
+ # }
419
+ response: requests.Response = requests.post(url=url,
420
+ data=body_data)
421
+ if response.status_code == 200:
422
+ # request succeeded
423
+ if logger:
424
+ logger.debug(msg=f"POST success, status {response.status_code}")
425
+ result = response.json()
426
+ else:
427
+ # request resulted in error
428
+ err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
429
+ if hasattr(response, "content") and response.content:
430
+ err_msg += f", content '{response.content}'"
431
+ if logger:
432
+ logger.error(msg=err_msg)
433
+ except Exception as e:
434
+ # the operation raised an exception
435
+ err_msg = exc_format(exc=e,
436
+ exc_info=sys.exc_info())
412
437
  if logger:
413
438
  logger.error(msg=err_msg)
414
- except Exception as e:
415
- # the operation raised an exception
416
- err_msg = exc_format(exc=e,
417
- exc_info=sys.exc_info())
418
- if logger:
419
- logger.error(msg=err_msg)
420
439
 
421
440
  if err_msg and isinstance(errors, list):
422
441
  errors.append(err_msg)
@@ -452,38 +471,39 @@ def __validate_and_store(iam_server: IamServer,
452
471
  # initialize the return variable
453
472
  result: tuple[str, str] | None = None
454
473
 
455
- # retrieve the IAM server's registry
456
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
457
- errors=errors,
458
- logger=logger)
459
- if registry:
460
- token: str = token_data.get("access_token")
461
- user_data["access-token"] = token
462
- # keep current refresh token if a new one is not provided
463
- if token_data.get("refresh_token"):
464
- user_data["refresh-token"] = token_data.get("refresh_token")
465
- user_data["access-expiration"] = now + token_data.get("expires_in")
466
- refresh_exp: int = user_data.get("refresh_expires_in")
467
- user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
468
- public_key: str = _get_public_key(iam_server=iam_server,
469
- errors=errors,
470
- logger=logger)
471
- if public_key:
472
- recipient_attr = registry["recipient_attr"]
473
- login_id = user_data.pop("login-id", None)
474
- claims: dict[str, dict[str, Any]] = token_validate(token=token,
475
- issuer=registry["base-url"],
476
- recipient_id=login_id,
477
- recipient_attr=recipient_attr,
478
- public_key=public_key,
479
- errors=errors,
480
- logger=logger)
481
- if claims:
482
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
483
- errors=errors,
484
- logger=logger)
485
- if users:
486
- user_id: str = login_id if login_id else claims["payload"][recipient_attr]
487
- users[user_id] = user_data
488
- result = (user_id, token)
474
+ with _iam_lock:
475
+ # retrieve the IAM server's registry
476
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
477
+ errors=errors,
478
+ logger=logger)
479
+ if registry:
480
+ token: str = token_data.get("access_token")
481
+ user_data["access-token"] = token
482
+ # keep current refresh token if a new one is not provided
483
+ if token_data.get("refresh_token"):
484
+ user_data["refresh-token"] = token_data.get("refresh_token")
485
+ user_data["access-expiration"] = now + token_data.get("expires_in")
486
+ refresh_exp: int = user_data.get("refresh_expires_in")
487
+ user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
488
+ public_key: str = _get_public_key(iam_server=iam_server,
489
+ errors=errors,
490
+ logger=logger)
491
+ if public_key:
492
+ recipient_attr = registry["recipient_attr"]
493
+ login_id = user_data.pop("login-id", None)
494
+ claims: dict[str, dict[str, Any]] = token_validate(token=token,
495
+ issuer=registry["base-url"],
496
+ recipient_id=login_id,
497
+ recipient_attr=recipient_attr,
498
+ public_key=public_key,
499
+ errors=errors,
500
+ logger=logger)
501
+ if claims:
502
+ users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
503
+ errors=errors,
504
+ logger=logger)
505
+ if users:
506
+ user_id: str = login_id if login_id else claims["payload"][recipient_attr]
507
+ users[user_id] = user_data
508
+ result = (user_id, token)
489
509
  return result