pypomes-iam 0.5.7__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
 
@@ -161,9 +277,10 @@ def _get_public_key(iam_server: IamServer,
161
277
  logger=logger)
162
278
  if registry:
163
279
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
164
- if now > registry["pk-expiration"]:
280
+ if now > registry[IamParam.PK_EXPIRATION]:
165
281
  # obtain the JWKS (JSON Web Key Set) from the token issuer
166
- url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
282
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
283
+ url: str = f"{base_url}/protocol/openid-connect/certs"
167
284
  if logger:
168
285
  logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
169
286
  logger.debug(msg=f"GET {url}")
@@ -174,9 +291,8 @@ def _get_public_key(iam_server: IamServer,
174
291
  if logger:
175
292
  logger.debug(msg=f"GET success, status {response.status_code}")
176
293
  # select the appropriate JWK
177
- reply: dict[str, Any] = response.json()
294
+ reply: dict[str, list[dict[str, str]]] = response.json()
178
295
  jwk: dict[str, str] | None = None
179
- # replay["keys"]: list[dict[str, str]]
180
296
  for key in reply["keys"]:
181
297
  if key.get("use") == "sig":
182
298
  jwk = key
@@ -185,11 +301,11 @@ def _get_public_key(iam_server: IamServer,
185
301
  # convert from 'JWK' to 'PEM' and save it for further use
186
302
  result = crypto_jwk_convert(jwk=jwk,
187
303
  fmt="PEM")
188
- registry["public-key"] = result
189
- lifetime: int = registry["pk-lifetime"] or 0
190
- registry["pk-expiration"] = now + lifetime if lifetime else sys.maxsize
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
191
307
  if logger:
192
- logger.debug(f"Public key obtained and saved")
308
+ logger.debug("Public key obtained and saved")
193
309
  else:
194
310
  msg = "Signature public key missing from the token issuer's JWKS"
195
311
  if logger:
@@ -212,7 +328,7 @@ def _get_public_key(iam_server: IamServer,
212
328
  if isinstance(errors, list):
213
329
  errors.append(msg)
214
330
  else:
215
- result = registry["public-key"]
331
+ result = registry[IamParam.PUBLIC_KEY]
216
332
 
217
333
  return result
218
334
 
@@ -267,10 +383,10 @@ def _get_user_data(iam_server: IamServer,
267
383
  result = users.get(user_id)
268
384
  if not result:
269
385
  result = {
270
- "access-token": None,
271
- "refresh-token": None,
272
- "access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
273
- "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
274
390
  }
275
391
  users[user_id] = result
276
392
  if logger:
@@ -296,10 +412,10 @@ def _get_iam_registry(iam_server: IamServer,
296
412
  result: dict[str, Any] | None
297
413
 
298
414
  match iam_server:
299
- case IamServer.IAM_JUSRBR:
300
- result = _IAM_SERVERS[IamServer.IAM_JUSRBR]
301
- case IamServer.IAM_KEYCLOAK:
302
- 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]
303
419
  case _:
304
420
  result = None
305
421
  msg = f"Unknown IAM server '{iam_server}'"
@@ -315,14 +431,14 @@ def _get_iam_users(iam_server: IamServer,
315
431
  errors: list[str] | None,
316
432
  logger: Logger | None) -> dict[str, dict[str, Any]]:
317
433
  """
318
- Retrieve the cache storage in *iam_server*'s registry.
434
+ Retrieve the users data storage in *iam_server*'s registry.
319
435
 
320
436
  :param iam_server: the reference registered *IAM* server
321
437
  :param errors: incidental error messages
322
438
  :param logger: optional logger
323
- :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
324
440
  """
325
441
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
326
442
  errors=errors,
327
443
  logger=logger)
328
- return registry["cache"]["users"] if registry else None
444
+ return registry[IamParam.USERS] if registry else None
@@ -0,0 +1,130 @@
1
+ from flask import Flask
2
+ from logging import Logger
3
+ from typing import Any
4
+
5
+ from .iam_common import (
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
11
+ )
12
+
13
+
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:
30
+ """
31
+ Establish the provided parameters for configuring the *IAM* server *iam_server*.
32
+
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.
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.
40
+
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
57
+ """
58
+
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
75
+
76
+ if public_key_lifetime:
77
+ IamParam.PK_LIFETIME = public_key_lifetime
78
+
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"])
105
+
106
+
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*.
113
+
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
122
+
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)
130
+ return result
@@ -4,7 +4,7 @@ from logging import Logger
4
4
  from typing import Any
5
5
 
6
6
  from .iam_common import (
7
- IamServer, _iam_lock,
7
+ IamServer, IamParam, _iam_lock,
8
8
  _get_iam_registry, _get_public_key,
9
9
  _iam_server_from_endpoint, _iam_server_from_issuer
10
10
  )
@@ -73,7 +73,7 @@ def __request_validate(request: Request) -> Response:
73
73
  errors=None,
74
74
  logger=__IAM_LOGGER)
75
75
  if registry:
76
- recipient_attr = registry["recipient-attr"]
76
+ recipient_attr = registry[IamParam.RECIPIENT_ATTR]
77
77
  public_key: str = _get_public_key(iam_server=iam_server,
78
78
  errors=None,
79
79
  logger=__IAM_LOGGER)
@@ -90,7 +90,7 @@ def __request_validate(request: Request) -> Response:
90
90
  elif __IAM_LOGGER:
91
91
  __IAM_LOGGER.error("; ".join(errors))
92
92
  if bad_token and __IAM_LOGGER:
93
- __IAM_LOGGER.error(f"authorization refused for token {token}")
93
+ __IAM_LOGGER.error(f"Authorization refused for token {token}")
94
94
 
95
95
  # deny the authorization
96
96
  if bad_token:
@@ -256,9 +256,9 @@ def service_callback() -> Response:
256
256
  else:
257
257
  result = jsonify({"user-id": token_data[0],
258
258
  "access-token": token_data[1]})
259
- # log the response
260
259
  if __IAM_LOGGER:
261
- __IAM_LOGGER.debug(msg=f"Response {result}, {result.get_data(as_text=True)}")
260
+ # log the response (the returned data is not logged, as it contains the token)
261
+ __IAM_LOGGER.debug(msg=f"Response {result}")
262
262
 
263
263
  return result
264
264
 
@@ -317,9 +317,9 @@ def service_token() -> Response:
317
317
  else:
318
318
  result = jsonify({"user-id": user_id,
319
319
  "access-token": token})
320
- # log the response
321
320
  if __IAM_LOGGER:
322
- __IAM_LOGGER.debug(msg=f"Response {result}, {result.get_data(as_text=True)}")
321
+ # log the response (the returned data is not logged, as it contains the token)
322
+ __IAM_LOGGER.debug(msg=f"Response {result}")
323
323
 
324
324
  return result
325
325
 
@@ -3,22 +3,42 @@ import requests
3
3
  import sys
4
4
  from base64 import b64encode
5
5
  from datetime import datetime
6
+ from enum import StrEnum
6
7
  from logging import Logger
7
8
  from pypomes_core import TZ_LOCAL, exc_format
8
9
  from threading import Lock
9
10
  from typing import Any, Final
10
11
 
12
+
13
+ class ProviderParam(StrEnum):
14
+ """
15
+ Parameters for configuring a *JWT* token provider.
16
+ """
17
+ URL = "url"
18
+ USER = "user"
19
+ PWD = "pwd"
20
+ CUSTOM_AUTH = "custom-auth"
21
+ HEADER_DATA = "headers-data"
22
+ BODY_DATA = "body-data"
23
+ ACCESS_TOKEN = "access-token"
24
+ ACCESS_EXPIRATION = "access-expiration"
25
+ REFRESH_TOKEN = "refresh-token"
26
+ REFRESH_EXPIRATION = "refresh-expiration"
27
+
28
+
11
29
  # structure:
12
30
  # {
13
31
  # <provider-id>: {
14
32
  # "url": <strl>,
15
33
  # "user": <str>,
16
34
  # "pwd": <str>,
17
- # "basic-auth": <bool>,
35
+ # "custom-auth": <bool>,
18
36
  # "headers-data": <dict[str, str]>,
19
37
  # "body-data": <dict[str, str],
20
38
  # "access-token": <str>,
21
- # "access-expiration": <timestamp>
39
+ # "access-expiration": <timestamp>,
40
+ # "refresh-token": <str>,
41
+ # "refresh-expiration": <timestamp>
22
42
  # }
23
43
  # }
24
44
  _provider_registry: Final[dict[str, dict[str, Any]]] = {}
@@ -58,16 +78,16 @@ def provider_register(provider_id: str,
58
78
 
59
79
  with _provider_lock:
60
80
  _provider_registry[provider_id] = {
61
- "url": auth_url,
62
- "user": auth_user,
63
- "pwd": auth_pwd,
64
- "custom-auth": custom_auth,
65
- "headers-data": headers_data,
66
- "body-data": body_data,
67
- "access-token": None,
68
- "access-expiration": 0,
69
- "refresh-token": None,
70
- "refresh-expiration": 0
81
+ ProviderParam.URL: auth_url,
82
+ ProviderParam.USER: auth_user,
83
+ ProviderParam.PWD: auth_pwd,
84
+ ProviderParam.CUSTOM_AUTH: custom_auth,
85
+ ProviderParam.HEADER_DATA: headers_data,
86
+ ProviderParam.BODY_DATA: body_data,
87
+ ProviderParam.ACCESS_TOKEN: None,
88
+ ProviderParam.ACCESS_EXPIRATION: 0,
89
+ ProviderParam.REFRESH_TOKEN: None,
90
+ ProviderParam.REFRESH_EXPIRATION: 0
71
91
  }
72
92
 
73
93
 
@@ -91,19 +111,19 @@ def provider_get_token(provider_id: str,
91
111
  provider: dict[str, Any] = _provider_registry.get(provider_id)
92
112
  if provider:
93
113
  now: float = datetime.now(tz=TZ_LOCAL).timestamp()
94
- if now > provider.get("access-expiration"):
95
- user: str = provider.get("user")
96
- pwd: str = provider.get("pwd")
97
- headers_data: dict[str, str] = provider.get("headers-data") or {}
98
- body_data: dict[str, str] = provider.get("body-data") or {}
99
- custom_auth: tuple[str, str] = provider.get("custom-auth")
114
+ if now > provider.get(ProviderParam.ACCESS_EXPIRATION):
115
+ user: str = provider.get(ProviderParam.USER)
116
+ pwd: str = provider.get(ProviderParam.PWD)
117
+ headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
118
+ body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
119
+ custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
100
120
  if custom_auth:
101
121
  body_data[custom_auth[0]] = user
102
122
  body_data[custom_auth[1]] = pwd
103
123
  else:
104
124
  enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
105
125
  headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
106
- url: str = provider.get("url")
126
+ url: str = provider.get(ProviderParam.URL)
107
127
  if logger:
108
128
  logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
109
129
  ensure_ascii=False)}")
@@ -130,14 +150,14 @@ def provider_get_token(provider_id: str,
130
150
  if logger:
131
151
  logger.debug(msg=f"POST success, status {response.status_code}")
132
152
  reply: dict[str, Any] = response.json()
133
- provider["access-token"] = reply.get("access_token")
134
- provider["access-expiration"] = now + int(reply.get("expires_in"))
135
- if reply.get("refresh_token"):
136
- provider["refresh-token"] = reply["refresh_token"]
153
+ provider[ProviderParam.ACCESS_TOKEN] = reply.get("access_token")
154
+ provider[ProviderParam.ACCESS_EXPIRATION] = now + int(reply.get("expires_in"))
155
+ if reply.get(ProviderParam.REFRESH_TOKEN):
156
+ provider[ProviderParam.REFRESH_TOKEN] = reply["refresh_token"]
137
157
  if reply.get("refresh_expires_in"):
138
- provider["refresh-expiration"] = now + int(reply.get("refresh_expires_in"))
158
+ provider[ProviderParam.REFRESH_EXPIRATION] = now + int(reply.get("refresh_expires_in"))
139
159
  else:
140
- provider["refresh-expiration"] = sys.maxsize
160
+ provider[ProviderParam.REFRESH_EXPIRATION] = sys.maxsize
141
161
  if logger:
142
162
  logger.debug(msg=f"POST {url}: status {response.status_code}")
143
163
  except Exception as e:
@@ -154,7 +174,7 @@ def provider_get_token(provider_id: str,
154
174
  if logger:
155
175
  logger.error(msg=err_msg)
156
176
  else:
157
- result = provider.get("access-token")
177
+ result = provider.get(ProviderParam.ACCESS_TOKEN)
158
178
 
159
179
  return result
160
180
 
@@ -117,8 +117,6 @@ def token_validate(token: str,
117
117
  "verify_nbf": False,
118
118
  "verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
119
119
  }
120
- if issuer:
121
- options["require"].append("iss")
122
120
  try:
123
121
  # raises:
124
122
  # InvalidTokenError: token is invalid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.5.7
3
+ Version: 0.5.8
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
@@ -0,0 +1,11 @@
1
+ pypomes_iam/__init__.py,sha256=f-2W_zrCmXExubJPExQrhAwGpiQCmybEC_wguRYFHsw,994
2
+ pypomes_iam/iam_actions.py,sha256=Bmd8rBg3948Afsg10B6B1ZrFY4wYtbxi55rX4Rlqiyk,39167
3
+ pypomes_iam/iam_common.py,sha256=I-HtwpvrhByTbOoSQrMktjpbYgeIPlYM1YC6wkFUhI4,18251
4
+ pypomes_iam/iam_pomes.py,sha256=BetEVGv41wkcP9E1wRvYiQgmJElDXH4Iz8qgf7iH6X0,5617
5
+ pypomes_iam/iam_services.py,sha256=IkCjrKDX1Ix7BiHh-BL3VKz5xogcNC8prXkHyJzQoZ8,15862
6
+ pypomes_iam/provider_pomes.py,sha256=N0nL9_hgHmAjG9JKFoXC33zk8b1ckPG1veu1jTp-2JE,8045
7
+ pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
8
+ pypomes_iam-0.5.8.dist-info/METADATA,sha256=Q60cQU69Gbay_IjFewESe9P4O4Z6mQ5tz_tYvw7yIMM,694
9
+ pypomes_iam-0.5.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ pypomes_iam-0.5.8.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
+ pypomes_iam-0.5.8.dist-info/RECORD,,