pypomes-iam 0.5.6__py3-none-any.whl → 0.5.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pypomes_iam/__init__.py CHANGED
@@ -3,19 +3,13 @@ from .iam_actions import (
3
3
  action_login, action_logout, action_token
4
4
  )
5
5
  from .iam_common import (
6
- IamServer
6
+ IamServer, IamParam
7
7
  )
8
8
  from .iam_pomes import (
9
- jwt_required
9
+ iam_setup, iam_get_token
10
10
  )
11
11
  from .iam_services import (
12
- logger_register
13
- )
14
- from .jusbr_pomes import (
15
- jusbr_setup, jusbr_get_token
16
- )
17
- from .keycloak_pomes import (
18
- keycloak_setup, keycloak_get_token
12
+ jwt_required, logger_register
19
13
  )
20
14
  from .provider_pomes import (
21
15
  provider_register, provider_get_token
@@ -29,15 +23,11 @@ __all__ = [
29
23
  "action_callback", "action_exchange",
30
24
  "action_login", "action_logout", "action_token",
31
25
  # iam_commons
32
- "IamServer",
26
+ "IamServer", "IamParam",
33
27
  # iam_pomes
34
- "jwt_required",
28
+ "iam_setup", "iam_get_token",
35
29
  # iam_services
36
- "logger_register",
37
- # jusbr_pomes
38
- "jusbr_setup", "jusbr_get_token",
39
- # keycloak_pomes
40
- "keycloak_setup", "keycloak_get_token",
30
+ "jwt_required", "logger_register",
41
31
  # provider_pomes
42
32
  "provider_register", "provider_get_token",
43
33
  # token_pomes
@@ -9,11 +9,11 @@ from pypomes_core import TZ_LOCAL, exc_format
9
9
  from typing import Any
10
10
 
11
11
  from .iam_common import (
12
- IamServer, _iam_lock,
12
+ IamServer, IamParam, UserParam, _iam_lock,
13
13
  _get_iam_users, _get_iam_registry, # _get_public_key,
14
14
  _get_login_timeout, _get_user_data
15
15
  )
16
- from .token_pomes import token_validate
16
+ from .token_pomes import token_get_claims, token_validate
17
17
 
18
18
 
19
19
  def action_login(iam_server: IamServer,
@@ -54,24 +54,25 @@ def action_login(iam_server: IamServer,
54
54
  errors=errors,
55
55
  logger=logger)
56
56
  if user_data:
57
- user_data["login-id"] = user_id
57
+ user_data[UserParam.LOGIN_ID] = user_id
58
58
  timeout: int = _get_login_timeout(iam_server=iam_server,
59
59
  errors=errors,
60
60
  logger=logger)
61
61
  if not errors:
62
- user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
62
+ user_data[UserParam.LOGIN_EXPIRATION] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
63
63
  if timeout else None
64
- redirect_uri: str = args.get("redirect-uri")
65
- user_data["redirect-uri"] = redirect_uri
64
+ redirect_uri: str = args.get(UserParam.REDIRECT_URI)
65
+ user_data[UserParam.REDIRECT_URI] = redirect_uri
66
66
 
67
67
  # build the login url
68
68
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
69
69
  errors=errors,
70
70
  logger=logger)
71
71
  if registry:
72
- result = (f"{registry["base-url"]}/protocol/openid-connect/auth"
72
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
73
+ result = (f"{base_url}/protocol/openid-connect/auth"
73
74
  f"?response_type=code&scope=openid"
74
- f"&client_id={registry["client-id"]}"
75
+ f"&client_id={registry[IamParam.CLIENT_ID]}"
75
76
  f"&redirect_uri={redirect_uri}"
76
77
  f"&state={oauth_state}")
77
78
  return result
@@ -137,24 +138,28 @@ def action_token(iam_server: IamServer,
137
138
  user_id=user_id,
138
139
  errors=errors,
139
140
  logger=logger)
140
- token: str = user_data["access-token"] if user_data else None
141
+ token: str = user_data[UserParam.ACCESS_TOKEN] if user_data else None
141
142
  if token:
142
- access_expiration: int = user_data.get("access-expiration")
143
+ access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
143
144
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
144
145
  if now < access_expiration:
145
146
  result = token
146
147
  else:
147
148
  # access token has expired
148
- refresh_token: str = user_data["refresh-token"]
149
+ refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
149
150
  if refresh_token:
150
- refresh_expiration = user_data["refresh-expiration"]
151
+ refresh_expiration = user_data[UserParam.REFRESH_EXPIRATION]
151
152
  if now < refresh_expiration:
153
+ header_data: dict[str, str] = {
154
+ "Content-Type": "application/json"
155
+ }
152
156
  body_data: dict[str, str] = {
153
157
  "grant_type": "refresh_token",
154
158
  "refresh_token": refresh_token
155
159
  }
156
160
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
157
161
  token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
162
+ header_data=header_data,
158
163
  body_data=body_data,
159
164
  errors=errors,
160
165
  logger=logger)
@@ -169,7 +174,7 @@ def action_token(iam_server: IamServer,
169
174
  result = token_info[1]
170
175
  else:
171
176
  # refresh token is no longer valid
172
- user_data["refresh-token"] = None
177
+ user_data[UserParam.REFRESH_TOKEN] = None
173
178
  else:
174
179
  # refresh token has expired
175
180
  err_msg = "Access and refresh tokens expired"
@@ -236,6 +241,9 @@ def action_callback(iam_server: IamServer,
236
241
  else:
237
242
  users.pop(oauth_state)
238
243
  code: str = args.get("code")
244
+ header_data: dict[str, str] = {
245
+ "Content-Type": "application/json"
246
+ }
239
247
  body_data: dict[str, Any] = {
240
248
  "grant_type": "authorization_code",
241
249
  "code": code,
@@ -243,6 +251,7 @@ def action_callback(iam_server: IamServer,
243
251
  }
244
252
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
245
253
  token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
254
+ header_data=header_data,
246
255
  body_data=body_data,
247
256
  errors=errors,
248
257
  logger=logger)
@@ -297,9 +306,8 @@ def action_exchange(iam_server: IamServer,
297
306
  user_id: str = args.get("user-id") or args.get("login")
298
307
 
299
308
  # obtain the token to be exchanged
300
- token: str = args.get("access-token")
301
-
302
- if user_id and token:
309
+ token: str = args.get("access-token") if user_id else None
310
+ if token:
303
311
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
304
312
  with _iam_lock:
305
313
  # retrieve the IAM server's registry
@@ -307,30 +315,166 @@ def action_exchange(iam_server: IamServer,
307
315
  errors=errors,
308
316
  logger=logger)
309
317
  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,
318
+ # make sure 'client_id' is linked to the token's 'token_sub' at the IAM server
319
+ __assert_link(iam_server=iam_server,
320
+ user_id=user_id,
321
+ token=token,
322
+ errors=errors,
323
+ logger=logger)
324
+ if not errors:
325
+ # exchange the token
326
+ header_data: dict[str, Any] = {
327
+ "Content-Type": "application/x-www-form-urlencoded"
328
+ }
329
+ body_data: dict[str, str] = {
330
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
331
+ "subject_token": token,
332
+ "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
333
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
334
+ "audience": registry[IamParam.CLIENT_ID],
335
+ "subject_issuer": "oidc"
336
+ }
337
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
338
+ token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
339
+ header_data=header_data,
340
+ body_data=body_data,
341
+ errors=errors,
342
+ logger=logger)
343
+ # validate and store the token data
344
+ if token_data:
345
+ user_data: dict[str, Any] = {}
346
+ result = __validate_and_store(iam_server=iam_server,
347
+ user_data=user_data,
348
+ token_data=token_data,
349
+ now=now,
350
+ errors=errors,
351
+ logger=logger)
352
+ else:
353
+ msg: str = "User identification or token not provided"
354
+ if logger:
355
+ logger.error(msg=msg)
356
+ if isinstance(errors, list):
357
+ errors.append(msg)
358
+
359
+ return result
360
+
361
+
362
+ def __assert_link(iam_server: IamServer,
363
+ user_id: str,
364
+ token: str,
365
+ errors: list[str] | None,
366
+ logger: Logger | None) -> None:
367
+ """
368
+ Make sure *iam_server* has a link associating *user_id* to an internal user identification.
369
+ This is a requirement for exchanging a token issued by a federated *IAM* server for an equivalent
370
+ one from *iam_server.
371
+
372
+ :param iam_server: the reference *IAM* server
373
+ :param user_id: the reference user identification
374
+ :param token: the reference token
375
+ :param errors: incidental errors
376
+ :param logger: optional logger
377
+ """
378
+ # obtain a token with administrative rights
379
+ admin_token: str = __get_administrative_token(iam_server=iam_server,
330
380
  errors=errors,
331
381
  logger=logger)
382
+ if admin_token:
383
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
384
+ errors=errors,
385
+ logger=logger)
386
+ # obtain the internal user identification for 'from_id'
387
+ url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
388
+ header_data: dict[str, str] = {
389
+ "Authorization": f"Bearer {admin_token}",
390
+ "Content-Type": "application/json"
391
+ }
392
+ params: dict[str, str] = {
393
+ "username": user_id,
394
+ "exact": "true"
395
+ }
396
+ users: dict[str, Any] = __get_for_data(url=url,
397
+ header_data=header_data,
398
+ params=params,
399
+ errors=errors,
400
+ logger=logger)
401
+ if users:
402
+ # verify whether the 'oidc' protocol is referred to in an
403
+ # association between 'from_id' and the internal user identification
404
+ internal_id: str = users.get("id")
405
+ url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
406
+ f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
407
+ providers: list[dict[str, Any]] = __get_for_data(url=url,
408
+ header_data=header_data,
409
+ params=None,
410
+ errors=errors,
411
+ logger=logger)
412
+ no_link: bool = True
413
+ for provider in providers:
414
+ if provider.get("identityProvider") == "oidc":
415
+ no_link = False
416
+ break
417
+ if no_link:
418
+ # link the identities
419
+ claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
420
+ errors=errors,
421
+ logger=logger)
422
+ if claims:
423
+ token_sub: str = claims["paylod"]["sub"]
424
+ url += "/oidc"
425
+ body_data: dict[str, Any] = {
426
+ "userId": token_sub,
427
+ "userName": user_id
428
+ }
429
+ __post_data(url=url,
430
+ header_data=header_data,
431
+ body_data=body_data,
432
+ errors=errors,
433
+ logger=logger)
434
+
435
+
436
+ def __get_administrative_token(iam_server: IamServer,
437
+ errors: list[str] | None,
438
+ logger: Logger | None) -> str:
439
+ """
440
+ Obtain a token with administrative rights from *iam_server*'s reference realm.
441
+
442
+ The reference realm is the realm specified at *iam_server*'s setup time. This operation requires
443
+ the realm administrator's identification and secret password to have also been provided.
444
+
445
+ :param iam_server: the reference *IAM* server
446
+ :param errors: incidental errors
447
+ :param logger: optional logger
448
+ :return: a token with administrative rights for the reference realm
449
+ """
450
+ # initialize the return variable
451
+ result: str | None = None
452
+
453
+ # obtain the IAM server's registry
454
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
455
+ errors=errors,
456
+ logger=logger)
457
+ if registry and registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
458
+ header_data: dict[str, str] = {
459
+ "Content-Type": "application/json"
460
+ }
461
+ body_data: dict[str, str] = {
462
+ "grant-type": "password",
463
+ "username": registry[IamParam.ADMIN_ID],
464
+ "password": registry[IamParam.ADMIN_SECRET],
465
+ "client_id": registry[IamParam.CLIENT_ID]
466
+ }
467
+ token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
468
+ header_data=header_data,
469
+ body_data=body_data,
470
+ errors=errors,
471
+ logger=logger)
472
+ if token_data:
473
+ # obtain the token
474
+ result = token_data["access_token"]
332
475
  else:
333
- msg: str = "User identification or token not provided"
476
+ msg: str = (f"To obtain token with administrative rights from '{iam_server}', "
477
+ f"the credentials for the realm administrator must be provided at setup time")
334
478
  if logger:
335
479
  logger.error(msg=msg)
336
480
  if isinstance(errors, list):
@@ -339,12 +483,165 @@ def action_exchange(iam_server: IamServer,
339
483
  return result
340
484
 
341
485
 
486
+ def __get_client_secret(iam_server: IamServer,
487
+ errors: list[str] | None,
488
+ logger: Logger | None) -> str:
489
+ """
490
+ Retrieve the client's secret password.
491
+
492
+ If it has not been provided at *iam_server*'s setup time, an attempt is made to obtain it
493
+ from the *IAM* server itself. This would require the realm administrator's identification and
494
+ secret password to have been provided, instead.
495
+
496
+ :param iam_server: the reference *IAM* server
497
+ :param errors: incidental errors
498
+ :param logger: optional logger
499
+ :return: the client's secret password, or *None* if error
500
+ """
501
+ # retrieve client's secret password stored in the IAM server's registry
502
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
503
+ errors=errors,
504
+ logger=logger)
505
+ result: str = registry[IamParam.CLIENT_SECRET] if registry else None
506
+
507
+ if not result and not errors:
508
+ # obtain a token with administrative rights
509
+ token: str = __get_administrative_token(iam_server=iam_server,
510
+ errors=errors,
511
+ logger=logger)
512
+ if token:
513
+ # obtain the client UUID
514
+ url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}/clients"
515
+ header_data: dict[str, str] = {
516
+ "Authorization": f"Bearer {token}",
517
+ "Content-Type": "application/json"
518
+ }
519
+ params: dict[str, str] = {
520
+ "clientId": registry[IamParam.CLIENT_ID]
521
+ }
522
+ clients: list[dict[str, Any]] = __get_for_data(url=url,
523
+ header_data=header_data,
524
+ params=params,
525
+ errors=errors,
526
+ logger=logger)
527
+ if clients:
528
+ # obtain the client's secret password
529
+ client_uuid: str = clients[0]["id"]
530
+ url += f"/{client_uuid}/client-secret"
531
+ reply: dict[str, Any] = __get_for_data(url=url,
532
+ header_data=header_data,
533
+ params=None,
534
+ errors=errors,
535
+ logger=logger)
536
+ if reply:
537
+ # store the client's secret password and return it
538
+ result = reply["value"]
539
+ registry[IamParam.CLIENT_ID] = result
540
+ return result
541
+
542
+
543
+ def __get_for_data(url: str,
544
+ header_data: dict[str, str],
545
+ params: dict[str, Any] | None,
546
+ errors: list[str] | None,
547
+ logger: Logger | None) -> Any:
548
+ """
549
+ Send a *GET* request to *url* and return the data obtained.
550
+
551
+ :param url: the target URL
552
+ :param header_data: the data to send in the header of the request
553
+ :param params: the query parameters to send in the request
554
+ :param errors: incidental errors
555
+ :param logger: optional logger
556
+ :return: the data requested, or *None* if error
557
+ """
558
+ # initialize the return variable
559
+ result: Any = None
560
+
561
+ # log the GET
562
+ if logger:
563
+ logger.debug(msg=f"GET {url}, {json.dumps(obj=params,
564
+ ensure_ascii=False)}")
565
+ try:
566
+ response: requests.Response = requests.get(url=url,
567
+ headers=header_data,
568
+ params=params)
569
+ if response.status_code == 200:
570
+ # request succeeded
571
+ result = response.json() or {}
572
+ if logger:
573
+ logger.debug(msg=f"GET success, {json.dumps(obj=result,
574
+ ensure_ascii=False)}")
575
+ else:
576
+ # request resulted in error
577
+ msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
578
+ if hasattr(response, "content") and response.content:
579
+ msg += f", content '{response.content}'"
580
+ if logger:
581
+ logger.error(msg=msg)
582
+ if isinstance(errors, list):
583
+ errors.append(msg)
584
+ except Exception as e:
585
+ # the operation raised an exception
586
+ msg: str = exc_format(exc=e,
587
+ exc_info=sys.exc_info())
588
+ if logger:
589
+ logger.error(msg=msg)
590
+ if isinstance(errors, list):
591
+ errors.append(msg)
592
+
593
+ return result
594
+
595
+
596
+ def __post_data(url: str,
597
+ header_data: dict[str, str],
598
+ body_data: dict[str, Any],
599
+ errors: list[str] | None,
600
+ logger: Logger | None) -> None:
601
+ """
602
+ Submit a *POST* request to *url*.
603
+
604
+ :param header_data: the data to send in the header of the request
605
+ :param body_data: the data to send in the body of the request
606
+ :param errors: incidental errors
607
+ :param logger: optional logger
608
+ """
609
+ # log the POST
610
+ if logger:
611
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
612
+ ensure_ascii=False)}")
613
+ try:
614
+ response: requests.Response = requests.get(url=url,
615
+ headers=header_data,
616
+ data=body_data)
617
+ if response.status_code >= 400:
618
+ # request resulted in error
619
+ msg = f"POST failure, status {response.status_code}, reason {response.reason}"
620
+ if hasattr(response, "content") and response.content:
621
+ msg += f", content '{response.content}'"
622
+ if logger:
623
+ logger.error(msg=msg)
624
+ if isinstance(errors, list):
625
+ errors.append(msg)
626
+ elif logger:
627
+ logger.debug(msg=f"POST success")
628
+ except Exception as e:
629
+ # the operation raised an exception
630
+ msg = exc_format(exc=e,
631
+ exc_info=sys.exc_info())
632
+ if logger:
633
+ logger.error(msg=msg)
634
+ if isinstance(errors, list):
635
+ errors.append(msg)
636
+
637
+
342
638
  def __post_for_token(iam_server: IamServer,
639
+ header_data: dict[str, str],
343
640
  body_data: dict[str, Any],
344
641
  errors: list[str] | None,
345
642
  logger: Logger | None) -> dict[str, Any] | None:
346
643
  """
347
- Send a POST request to obtain the authentication token data, and return the data received.
644
+ Send a *POST* request to *iam_server* and return the authentication token data obtained.
348
645
 
349
646
  For token acquisition, *body_data* will have the attributes:
350
647
  - "grant_type": "authorization_code"
@@ -363,9 +660,14 @@ def __post_for_token(iam_server: IamServer,
363
660
  - "audience": <client-id>,
364
661
  - "subject_issuer": "oidc"
365
662
 
663
+ For administrative token acquisition, *body_data* will have the attributes:
664
+ - "grant_type": "password"
665
+ - "username": <realm-administrator-identification>
666
+ - "password": <realm-administrator-secret>
667
+
366
668
  These attributes are then added to *body_data*:
367
- - "client_id": <client-id>,
368
- - "client_secret": <client-secret>,
669
+ - "client_id": <client-id>
670
+ - "client_secret": <client-secret> <- except for acquiring administrative tokens
369
671
 
370
672
  If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
371
673
  Otherwise, *errors* will contain the appropriate error message.
@@ -380,6 +682,7 @@ def __post_for_token(iam_server: IamServer,
380
682
  }
381
683
 
382
684
  :param iam_server: the reference registered *IAM* server
685
+ :param header_data: the data to send in the header of the request
383
686
  :param body_data: the data to send in the body of the request
384
687
  :param errors: incidental errors
385
688
  :param logger: optional logger
@@ -396,18 +699,23 @@ def __post_for_token(iam_server: IamServer,
396
699
  logger=logger)
397
700
  if registry:
398
701
  # complete the data to send in body of request
399
- body_data["client_id"] = registry["client-id"]
400
- client_secret: str = registry["client-secret"]
702
+ body_data["client_id"] = registry[IamParam.CLIENT_ID]
401
703
 
402
- # obtain the token
403
- url: str = registry["base-url"] + "/protocol/openid-connect/token"
704
+ # build the URL
705
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
706
+ url: str = f"{base_url}/protocol/openid-connect/token"
404
707
 
405
708
  # log the POST ('client_secret' data must not be shown in log)
406
709
  if logger:
407
710
  logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
408
711
  ensure_ascii=False)}")
409
- if client_secret:
712
+ client_secret: str = __get_client_secret(iam_server=iam_server,
713
+ errors=errors,
714
+ logger=logger)
715
+ if body_data["grant_type"] != "password" and client_secret:
410
716
  body_data["client_secret"] = client_secret
717
+
718
+ # obtain the token
411
719
  try:
412
720
  # typical return on a token request:
413
721
  # {
@@ -418,6 +726,7 @@ def __post_for_token(iam_server: IamServer,
418
726
  # "refesh_expires_in": <number-of-seconds>
419
727
  # }
420
728
  response: requests.Response = requests.post(url=url,
729
+ headers=header_data,
421
730
  data=body_data)
422
731
  if response.status_code == 200:
423
732
  # request succeeded
@@ -490,10 +799,11 @@ def __validate_and_store(iam_server: IamServer,
490
799
  # public_key: str = _get_public_key(iam_server=iam_server,
491
800
  # errors=errors,
492
801
  # logger=logger)
493
- recipient_attr = registry["recipient-attr"]
802
+ recipient_attr = registry[IamParam.RECIPIENT_ATTR]
494
803
  login_id = user_data.pop("login-id", None)
804
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
495
805
  claims: dict[str, dict[str, Any]] = token_validate(token=token,
496
- issuer=registry["base-url"],
806
+ issuer=base_url,
497
807
  recipient_id=login_id,
498
808
  recipient_attr=recipient_attr,
499
809
  # public_key=public_key,