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/iam_common.py CHANGED
@@ -1,9 +1,12 @@
1
1
  import requests
2
2
  import sys
3
3
  from datetime import datetime
4
- from enum import StrEnum
4
+ from enum import StrEnum, auto
5
5
  from logging import Logger
6
- from pypomes_core import TZ_LOCAL, exc_format
6
+ from pypomes_core import (
7
+ APP_PREFIX, TZ_LOCAL,
8
+ env_get_int, env_get_str, env_get_enum, env_get_enums, exc_format
9
+ )
7
10
  from pypomes_crypto import crypto_jwk_convert
8
11
  from threading import RLock
9
12
  from typing import Any, Final
@@ -13,8 +16,120 @@ class IamServer(StrEnum):
13
16
  """
14
17
  Supported IAM servers.
15
18
  """
16
- IAM_JUSRBR = "iam-jusbr",
17
- IAM_KEYCLOAK = "iam-keycloak"
19
+ JUSRBR = auto()
20
+ KEYCLOAK = auto()
21
+
22
+
23
+ class IamParam(StrEnum):
24
+ """
25
+ Parameters for configuring *IAM* servers.
26
+ """
27
+ ADMIN_ID = "admin-id"
28
+ ADMIN_SECRET = "admin-secret"
29
+ CLIENT_ID = "client-id"
30
+ CLIENT_REALM = "client-realm"
31
+ CLIENT_SECRET = "client-secret"
32
+ ENDPOINT_CALLBACK = "endpoint-callback"
33
+ ENDPOINT_LOGIN = "endpoint-login"
34
+ ENDPOINT_LOGOUT = "endpoint_logout"
35
+ ENDPOINT_TOKEN = "endpoint-token"
36
+ ENDPOINT_EXCHANGE = "endpoint-exchange"
37
+ LOGIN_TIMEOUT = "login-timeout"
38
+ PK_EXPIRATION = "pk-expiration"
39
+ PK_LIFETIME = "pk-lifetime"
40
+ PUBLIC_KEY = "public-key"
41
+ RECIPIENT_ATTR = "recipient-attr"
42
+ URL_BASE = "url-base"
43
+ USERS = "users"
44
+
45
+
46
+ class UserParam(StrEnum):
47
+ """
48
+ Parameters for handling *IAM* users.
49
+ """
50
+ ACCESS_TOKEN = "access-token"
51
+ REFRESH_TOKEN = "refresh-token"
52
+ ACCESS_EXPIRATION = "access-expiration"
53
+ REFRESH_EXPIRATION = "refresh-expiration"
54
+ # transient attributes
55
+ LOGIN_EXPIRATION = "login-expiration"
56
+ LOGIN_ID = "login-id"
57
+ REDIRECT_URI = "redirect-uri"
58
+
59
+
60
+ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
61
+ """
62
+ Establish the configuration data for select *IAM* servers, from environment variables.
63
+
64
+ The preferred way to specify configuration parameters is dynamically with *iam_setup()*;.
65
+ Specifying configuration parameters with environment variables can be done in two ways:
66
+
67
+ 1. for a single *IAM* server, specify the data set
68
+ - *<APP_PREFIX>_IAM_SERVER* (required, one of *jusbr*, *keycloak*)
69
+ - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed only if administrative duties are performed)
70
+ - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed only if administrative duties are performed)
71
+ - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
72
+ - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
73
+ - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
74
+ - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (optional)
75
+ - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (optional)
76
+ - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (optional)
77
+ - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (optional)
78
+ - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (optional)
79
+ - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
80
+ - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
81
+ - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
82
+ - *<APP_PREFIX>_IAM_URL_BASE* (required)
83
+
84
+ 2. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
85
+ as they are reserved for internal use
86
+
87
+ 3. for multiple *IAM* servers, specify a comma-separated list of servers in
88
+ *<APP_PREFIX>_IAM_SERVERS*, and for each server, specify the data set above,
89
+ respectively replacing *_IAM_* with *_JUSBR_* or *_KEYCLOAK_*, for the servers listed above
90
+
91
+ :return: the configuration data for the selected *IAM* servers
92
+ """
93
+ # initialize the return valiable
94
+ result: dict[IamServer, dict[IamParam, Any]] = {}
95
+
96
+ servers: list[IamServer] = []
97
+ single_server: IamServer = env_get_enum(key=f"{APP_PREFIX}_IAM_SERVER",
98
+ enum_class=IamServer)
99
+ if single_server:
100
+ default_setup: bool = True
101
+ servers.append(single_server)
102
+ else:
103
+ default_setup: bool = False
104
+ multi_servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
105
+ enum_class=IamServer)
106
+ if multi_servers:
107
+ servers.extend(multi_servers)
108
+
109
+ for server in servers:
110
+ if default_setup:
111
+ prefix: str = "IAM"
112
+ default_setup = False
113
+ else:
114
+ prefix: str = server
115
+ result[server] = {
116
+ IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
117
+ IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_PWD"),
118
+ IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
119
+ IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
120
+ IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
121
+ IamParam.LOGIN_TIMEOUT: env_get_int(key=f"{APP_PREFIX}_{prefix}_CLIENT_TIMEOUT"),
122
+ IamParam.ENDPOINT_CALLBACK: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK"),
123
+ IamParam.ENDPOINT_LOGIN: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN"),
124
+ IamParam.ENDPOINT_LOGOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGOUT"),
125
+ IamParam.ENDPOINT_TOKEN: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_TOKEN"),
126
+ IamParam.ENDPOINT_EXCHANGE: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE"),
127
+ IamParam.PK_LIFETIME: env_get_str(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
128
+ IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
129
+ IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_BASE")
130
+ }
131
+
132
+ return result
18
133
 
19
134
 
20
135
  # registry structure:
@@ -23,32 +138,32 @@ class IamServer(StrEnum):
23
138
  # "base-url": <str>,
24
139
  # "client-id": <str>,
25
140
  # "client-secret": <str>,
141
+ # "client-realm": <str,
26
142
  # "client-timeout": <int>,
27
143
  # "recipient-attr": <str>,
28
144
  # "public-key": <str>,
29
145
  # "pk-lifetime": <int>,
30
146
  # "pk-expiration": <int>,
31
- # "cache": <FIFOCache>
147
+ # "users": {}
32
148
  # },
33
149
  # ...
34
150
  # }
35
- # data in "cache":
151
+ # data in "users":
36
152
  # {
37
- # "users": {
38
- # "<user-id>": {
39
- # "access-token": <str>
40
- # "refresh-token": <str>
41
- # "access-expiration": <timestamp>,
42
- # "refresh-expiration": <timestamp>,
43
- # # transient attributes:
44
- # "login-expiration": <timestamp>,
45
- # "login-id": <str>,
46
- # "redirect-uri": <str>
47
- # }
48
- # },
153
+ # "<user-id>": {
154
+ # "access-token": <str>
155
+ # "refresh-token": <str>
156
+ # "access-expiration": <timestamp>,
157
+ # "refresh-expiration": <timestamp>,
158
+ # # transient attributes
159
+ # "login-expiration": <timestamp>,
160
+ # "login-id": <str>,
161
+ # "redirect-uri": <str>
162
+ # },
49
163
  # ...
50
164
  # }
51
- _IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
165
+ _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
166
+
52
167
 
53
168
  # the lock protecting the data in '_IAM_SERVERS'
54
169
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
@@ -66,15 +181,15 @@ def _iam_server_from_endpoint(endpoint: str,
66
181
  :param logger: optional logger
67
182
  :return: the corresponding *IAM* server, or *None* if one could not be obtained
68
183
  """
69
- # declare the return variable
70
- result: IamServer | None
184
+ # initialize the return variable
185
+ result: IamServer | None = None
71
186
 
72
- if endpoint.startswith("jusbr"):
73
- result = IamServer.IAM_JUSRBR
74
- elif endpoint.startswith("keycloak"):
75
- result = IamServer.IAM_KEYCLOAK
76
- else:
77
- result = None
187
+ for iam_server in _IAM_SERVERS:
188
+ if endpoint.startswith(iam_server):
189
+ result = IamServer.JUSRBR
190
+ break
191
+
192
+ if not result:
78
193
  msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
79
194
  if logger:
80
195
  logger.error(msg=msg)
@@ -98,8 +213,9 @@ def _iam_server_from_issuer(issuer: str,
98
213
  # initialize the return variable
99
214
  result: IamServer | None = None
100
215
 
101
- for iam_server, server_data in _IAM_SERVERS.items():
102
- if server_data["base-url"] == issuer:
216
+ for iam_server, registry in _IAM_SERVERS.items():
217
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
218
+ if base_url == issuer:
103
219
  result = IamServer(iam_server)
104
220
  break
105
221
 
@@ -119,12 +235,39 @@ def _get_public_key(iam_server: IamServer,
119
235
  """
120
236
  Obtain the public key used by *iam_server* to sign the authentication tokens.
121
237
 
122
- The public key is saved in *iam_server*'s registry.
238
+ This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
239
+ containing the public keys used for various purposes, as indicated in the attribute *use*:
240
+ - *enc*: the key is intended for encryption
241
+ - *sig*: the key is intended for digital signature
242
+ - *wrap*: the key is intended for key wrapping
243
+
244
+ A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
245
+ {
246
+ "keys": [
247
+ {
248
+ "kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
249
+ "kty": "RSA",
250
+ "alg": "RSA256",
251
+ "use": "sig",
252
+ "n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
253
+ "e": "AQAB",
254
+ "x5c": [
255
+ "MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
256
+ ],
257
+ "x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
258
+ "x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
259
+ },
260
+ ...
261
+ ]
262
+ }
263
+
264
+ Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
265
+ to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
123
266
 
124
267
  :param iam_server: the reference registered *IAM* server
125
268
  :param errors: incidental error messages
126
269
  :param logger: optional logger
127
- :return: the public key in *PEM* format, or *None* if the server is unknown
270
+ :return: the public key in *PEM* format, or *None* if error
128
271
  """
129
272
  # initialize the return variable
130
273
  result: str | None = None
@@ -134,10 +277,12 @@ def _get_public_key(iam_server: IamServer,
134
277
  logger=logger)
135
278
  if registry:
136
279
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
137
- if now > registry["pk-expiration"]:
138
- # obtain a new public key
139
- url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
280
+ if now > registry[IamParam.PK_EXPIRATION]:
281
+ # obtain the JWKS (JSON Web Key Set) from the token issuer
282
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
283
+ url: str = f"{base_url}/protocol/openid-connect/certs"
140
284
  if logger:
285
+ logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
141
286
  logger.debug(msg=f"GET {url}")
142
287
  try:
143
288
  response: requests.Response = requests.get(url=url)
@@ -145,12 +290,28 @@ def _get_public_key(iam_server: IamServer,
145
290
  # request succeeded
146
291
  if logger:
147
292
  logger.debug(msg=f"GET success, status {response.status_code}")
148
- reply: dict[str, Any] = response.json()
149
- result = crypto_jwk_convert(jwk=reply["keys"][0],
150
- fmt="PEM")
151
- registry["public-key"] = result
152
- lifetime: int = registry["pk-lifetime"] or 0
153
- registry["pk-expiration"] = now + lifetime
293
+ # select the appropriate JWK
294
+ reply: dict[str, list[dict[str, str]]] = response.json()
295
+ jwk: dict[str, str] | None = None
296
+ for key in reply["keys"]:
297
+ if key.get("use") == "sig":
298
+ jwk = key
299
+ break
300
+ if jwk:
301
+ # convert from 'JWK' to 'PEM' and save it for further use
302
+ result = crypto_jwk_convert(jwk=jwk,
303
+ fmt="PEM")
304
+ registry[IamParam.PUBLIC_KEY] = result
305
+ lifetime: int = registry[IamParam.PK_LIFETIME] or 0
306
+ registry[IamParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
307
+ if logger:
308
+ logger.debug("Public key obtained and saved")
309
+ else:
310
+ msg = "Signature public key missing from the token issuer's JWKS"
311
+ if logger:
312
+ logger.error(msg=msg)
313
+ if isinstance(errors, list):
314
+ errors.append(msg)
154
315
  elif logger:
155
316
  msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
156
317
  if hasattr(response, "content") and response.content:
@@ -167,7 +328,7 @@ def _get_public_key(iam_server: IamServer,
167
328
  if isinstance(errors, list):
168
329
  errors.append(msg)
169
330
  else:
170
- result = registry["public-key"]
331
+ result = registry[IamParam.PUBLIC_KEY]
171
332
 
172
333
  return result
173
334
 
@@ -222,10 +383,10 @@ def _get_user_data(iam_server: IamServer,
222
383
  result = users.get(user_id)
223
384
  if not result:
224
385
  result = {
225
- "access-token": None,
226
- "refresh-token": None,
227
- "access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
228
- "refresh-expiration": sys.maxsize
386
+ UserParam.ACCESS_TOKEN: None,
387
+ UserParam.REFRESH_TOKEN: None,
388
+ UserParam.ACCESS_EXPIRATION: int(datetime.now(tz=TZ_LOCAL).timestamp()),
389
+ UserParam.REFRESH_EXPIRATION: sys.maxsize
229
390
  }
230
391
  users[user_id] = result
231
392
  if logger:
@@ -251,10 +412,10 @@ def _get_iam_registry(iam_server: IamServer,
251
412
  result: dict[str, Any] | None
252
413
 
253
414
  match iam_server:
254
- case IamServer.IAM_JUSRBR:
255
- result = _IAM_SERVERS[IamServer.IAM_JUSRBR]
256
- case IamServer.IAM_KEYCLOAK:
257
- result = _IAM_SERVERS[IamServer.IAM_KEYCLOAK]
415
+ case IamServer.JUSRBR:
416
+ result = _IAM_SERVERS[IamServer.JUSRBR]
417
+ case IamServer.KEYCLOAK:
418
+ result = _IAM_SERVERS[IamServer.KEYCLOAK]
258
419
  case _:
259
420
  result = None
260
421
  msg = f"Unknown IAM server '{iam_server}'"
@@ -270,14 +431,14 @@ def _get_iam_users(iam_server: IamServer,
270
431
  errors: list[str] | None,
271
432
  logger: Logger | None) -> dict[str, dict[str, Any]]:
272
433
  """
273
- Retrieve the cache storage in *iam_server*'s registry.
434
+ Retrieve the users data storage in *iam_server*'s registry.
274
435
 
275
436
  :param iam_server: the reference registered *IAM* server
276
437
  :param errors: incidental error messages
277
438
  :param logger: optional logger
278
- :return: the cache storage in *iam_server*'s registry, or *None* if the server is unknown
439
+ :return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
279
440
  """
280
441
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
281
442
  errors=errors,
282
443
  logger=logger)
283
- return registry["cache"]["users"] if registry else None
444
+ return registry[IamParam.USERS] if registry else None
pypomes_iam/iam_pomes.py CHANGED
@@ -1,82 +1,130 @@
1
- from flask import Request, Response, request
1
+ from flask import Flask
2
+ from logging import Logger
2
3
  from typing import Any
3
4
 
4
5
  from .iam_common import (
5
- IamServer, _iam_lock, _get_iam_registry,
6
- _iam_server_from_issuer # _get_public_key
6
+ _IAM_SERVERS, IamServer, IamParam, _iam_lock
7
+ )
8
+ from .iam_actions import action_token
9
+ from .iam_services import (
10
+ service_login, service_logout, service_callback, service_exchange, service_token
7
11
  )
8
- from .token_pomes import token_get_claims, token_validate
9
-
10
12
 
11
- def jwt_required(func: callable) -> callable:
12
- """
13
- Create a decorator to authenticate service endpoints with JWT tokens.
14
13
 
15
- :param func: the function being decorated
14
+ def iam_setup(flask_app: Flask,
15
+ iam_server: IamServer,
16
+ base_url: str,
17
+ client_id: str,
18
+ client_realm: str,
19
+ recipient_attribute: str,
20
+ client_secret: str = None,
21
+ login_timeout: int = None,
22
+ admin_id: str = None,
23
+ admin_secret: str = None,
24
+ public_key_lifetime: int = None,
25
+ callback_endpoint: str = None,
26
+ login_endpoint: str = None,
27
+ logout_endpoint: str = None,
28
+ token_endpoint: str = None,
29
+ exchange_endpoint: str = None) -> None:
16
30
  """
17
- # ruff: noqa: ANN003 - Missing type annotation for *{name}
18
- def wrapper(*args, **kwargs) -> Response:
19
- response: Response = __request_validate(request=request)
20
- return response if response else func(*args, **kwargs)
21
-
22
- # prevent a rogue error ("View function mapping is overwriting an existing endpoint function")
23
- wrapper.__name__ = func.__name__
31
+ Establish the provided parameters for configuring the *IAM* server *iam_server*.
24
32
 
25
- return wrapper
33
+ The parameters *admin_id* and *admin_* are required only if administrative are task are planned.
34
+ The optional parameter *client_timeout* refers to the maximum time in seconds allowed for the
35
+ user to login at the *IAM* server's login page, and defaults to no time limit.
26
36
 
37
+ The parameter *client_secret* is required in most requests to the *IAM* server. In the case
38
+ it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
39
+ the first time it is needed.
27
40
 
28
- def __request_validate(request: Request) -> Response:
41
+ :param flask_app: the Flask application
42
+ :param iam_server: identifies the supported *IAM* server (*jusbr* or *keycloak*)
43
+ :param base_url: base URL to request services
44
+ :param client_id: the client's identification with the *IAM* server
45
+ :param client_realm: the client realm
46
+ :param recipient_attribute: attribute in the token's payload holding the token's subject
47
+ :param client_secret: the client's password with the *IAM* server
48
+ :param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
49
+ :param admin_id: identifies the realm administrator
50
+ :param admin_secret: password for the realm administrator
51
+ :param public_key_lifetime: how long to use *IAM* server's public key, before refreshing it (in seconds)
52
+ :param callback_endpoint: endpoint for the callback from the front end
53
+ :param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
54
+ :param logout_endpoint: endpoint for terminating user access
55
+ :param token_endpoint: endpoint for retrieving authentication token
56
+ :param exchange_endpoint: endpoint for requesting token exchange
29
57
  """
30
- Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
31
58
 
32
- This implementation assumes that HTTP requests are handled with the *Flask* framework.
59
+ # configure the Keycloak registry
60
+ with _iam_lock:
61
+ _IAM_SERVERS[iam_server] = {
62
+ IamParam.URL_BASE: base_url,
63
+ IamParam.CLIENT_ID: client_id,
64
+ IamParam.CLIENT_REALM: client_realm,
65
+ IamParam.CLIENT_SECRET: client_secret,
66
+ IamParam.LOGIN_TIMEOUT: login_timeout,
67
+ IamParam.RECIPIENT_ATTR: recipient_attribute,
68
+ IamParam.PK_EXPIRATION: 0,
69
+ IamParam.PUBLIC_KEY: None,
70
+ IamParam.USERS: {}
71
+ }
72
+ if admin_id and admin_secret:
73
+ IamParam.ADMIN_ID = admin_id
74
+ IamParam.ADMIN_SECRET = admin_secret
33
75
 
34
- :param request: the *request* to be verified
35
- :return: *None* if the *request* is valid, otherwise a *Response* reporting the error
36
- """
37
- # initialize the return variable
38
- result: Response | None = None
76
+ if public_key_lifetime:
77
+ IamParam.PK_LIFETIME = public_key_lifetime
39
78
 
40
- # retrieve the authorization from the request header
41
- auth_header: str = request.headers.get("Authorization")
79
+ # establish the endpoints
80
+ if callback_endpoint:
81
+ flask_app.add_url_rule(rule=callback_endpoint,
82
+ endpoint=f"{iam_server}-callback",
83
+ view_func=service_callback,
84
+ methods=["GET"])
85
+ if login_endpoint:
86
+ flask_app.add_url_rule(rule=login_endpoint,
87
+ endpoint=f"{iam_server}-login",
88
+ view_func=service_login,
89
+ methods=["GET"])
90
+ if logout_endpoint:
91
+ flask_app.add_url_rule(rule=logout_endpoint,
92
+ endpoint=f"{iam_server}-logout",
93
+ view_func=service_logout,
94
+ methods=["GET"])
95
+ if token_endpoint:
96
+ flask_app.add_url_rule(rule=token_endpoint,
97
+ endpoint=f"{iam_server}-token",
98
+ view_func=service_token,
99
+ methods=["GET"])
100
+ if exchange_endpoint:
101
+ flask_app.add_url_rule(rule=exchange_endpoint,
102
+ endpoint=f"{iam_server}-exchange",
103
+ view_func=service_exchange,
104
+ methods=["POST"])
42
105
 
43
- # validate the authorization token
44
- bad_token: bool = True
45
- if auth_header and auth_header.startswith("Bearer "):
46
- # extract and validate the JWT access token
47
- token: str = auth_header.split(" ")[1]
48
- claims: dict[str, Any] = token_get_claims(token=token)
49
- if claims:
50
- issuer: str = claims["payload"].get("iss")
51
- recipient_attr: str | None = None
52
- recipient_id: str = request.values.get("user-id") or request.values.get("login")
53
- with _iam_lock:
54
- iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
55
- errors=None,
56
- logger=None)
57
- # public_key: str = _get_public_key(iam_server=iam_server,
58
- # errors=errors,
59
- # logger=logger)
60
- public_key = None
61
106
 
62
- # validate the token's recipient only if a user identification is provided
63
- if recipient_id:
64
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
65
- errors=None,
66
- logger=None)
67
- recipient_attr = registry["recipient-attr"]
107
+ def iam_get_token(iam_server: IamServer,
108
+ user_id: str,
109
+ errors: list[str] = None,
110
+ logger: Logger = None) -> str:
111
+ """
112
+ Retrieve an authentication token for *user_id*.
68
113
 
69
- # validate the token
70
- if token_validate(token=token,
71
- issuer=issuer,
72
- recipient_id=recipient_id,
73
- recipient_attr=recipient_attr,
74
- public_key=public_key):
75
- # token is valid
76
- bad_token = False
114
+ :param iam_server: identifies the *IAM* server
115
+ :param user_id: identifies the user
116
+ :param errors: incidental errors
117
+ :param logger: optional logger
118
+ :return: the uthentication tokem
119
+ """
120
+ # declare the return variable
121
+ result: str
77
122
 
78
- # deny the authorization
79
- if bad_token:
80
- result = Response(response="Authorization failed",
81
- status=401)
123
+ # retrieve the token
124
+ args: dict[str, Any] = {"user-id": user_id}
125
+ with _iam_lock:
126
+ result = action_token(iam_server=iam_server,
127
+ args=args,
128
+ errors=errors,
129
+ logger=logger)
82
130
  return result