pypomes-iam 0.8.6__tar.gz → 0.9.3__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.8.6
3
+ Version: 0.9.3
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
@@ -13,5 +13,5 @@ Requires-Python: >=3.12
13
13
  Requires-Dist: flask>=3.1.2
14
14
  Requires-Dist: pyjwt>=2.10.1
15
15
  Requires-Dist: pypomes-core>=2.8.6
16
- Requires-Dist: pypomes-crypto>=0.4.8
16
+ Requires-Dist: pypomes-crypto>=0.5.0
17
17
  Requires-Dist: requests>=2.32.5
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.8.6"
9
+ version = "0.9.3"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "Flask>=3.1.2",
23
23
  "PyJWT>=2.10.1",
24
24
  "pypomes-core>=2.8.6",
25
- "pypomes-crypto>=0.4.8",
25
+ "pypomes-crypto>=0.5.0",
26
26
  "requests>=2.32.5"
27
27
  ]
28
28
 
@@ -19,9 +19,6 @@ from .provider_pomes import (
19
19
  service_get_token, provider_get_token,
20
20
  iam_setup_provider, provider_setup_endpoint, provider_setup_logger
21
21
  )
22
- from .token_pomes import (
23
- token_get_claims, token_get_values, token_validate
24
- )
25
22
 
26
23
  __all__ = [
27
24
  # iam_actions
@@ -40,8 +37,6 @@ __all__ = [
40
37
  "IamProvider", "ProviderParam",
41
38
  "service_get_token", "provider_get_token",
42
39
  "iam_setup_provider", "provider_setup_endpoint", "provider_setup_logger",
43
- # token_pomes
44
- "token_get_claims", "token_get_values", "token_validate"
45
40
  ]
46
41
 
47
42
  from importlib.metadata import version
@@ -6,6 +6,7 @@ import sys
6
6
  from datetime import datetime
7
7
  from logging import Logger
8
8
  from pypomes_core import TZ_LOCAL, exc_format
9
+ from pypomes_crypto import jwt_get_values, jwt_validate
9
10
  from typing import Any
10
11
 
11
12
  from .iam_common import (
@@ -13,7 +14,6 @@ from .iam_common import (
13
14
  _get_iam_users, _get_iam_registry, _get_public_key,
14
15
  _get_login_timeout, _get_user_data, _iam_server_from_issuer
15
16
  )
16
- from .token_pomes import token_get_values, token_validate
17
17
 
18
18
 
19
19
  def iam_login(iam_server: IamServer,
@@ -56,7 +56,7 @@ def iam_login(iam_server: IamServer,
56
56
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
57
57
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
58
58
  if target_idp:
59
- oauth_state += f"#idp={target_idp}"
59
+ oauth_state += f"_{target_idp}"
60
60
 
61
61
  with _iam_lock:
62
62
  # retrieve the user data from the IAM server's registry
@@ -322,8 +322,8 @@ def iam_callback(iam_server: IamServer,
322
322
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
323
323
  errors.append("Operation timeout")
324
324
  else:
325
- pos: int = oauth_state.rfind("#idp=")
326
- target_idp: str = oauth_state[pos+4:] if pos > 0 else None
325
+ pos: int = oauth_state.rfind("_")
326
+ target_idp: str = oauth_state[pos+1:] if pos > 0 else None
327
327
  target_iam: IamServer = IamServer(target_idp) if target_idp in IamServer else None
328
328
  target_data: dict[str, Any] = user_data.copy() if target_iam else None
329
329
  users.pop(oauth_state)
@@ -421,10 +421,10 @@ def iam_exchange(iam_server: IamServer,
421
421
 
422
422
  # obtain the token to be exchanged
423
423
  token: str = args.get("access-token") if user_id else None
424
- token_issuer: tuple[str] = token_get_values(token=token,
425
- keys=("iss",),
426
- errors=errors,
427
- logger=logger)
424
+ token_issuer: tuple[str] = jwt_get_values(token=token,
425
+ keys=("iss",),
426
+ errors=errors,
427
+ logger=logger)
428
428
  if not errors:
429
429
  with _iam_lock:
430
430
  # retrieve the IAM server's registry
@@ -604,10 +604,10 @@ def __assert_link(iam_server: IamServer,
604
604
  break
605
605
  if no_link:
606
606
  # link the identities
607
- token_sub: tuple[str] = token_get_values(token=token,
608
- keys=("sub",),
609
- errors=errors,
610
- logger=logger)
607
+ token_sub: tuple[str] = jwt_get_values(token=token,
608
+ keys=("sub",),
609
+ errors=errors,
610
+ logger=logger)
611
611
  if token_sub:
612
612
  if logger:
613
613
  logger.debug(msg="Creating an association between identifications "
@@ -1023,13 +1023,13 @@ def __validate_and_store(iam_server: IamServer,
1023
1023
  recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
1024
1024
  login_id = user_data.pop("login-id", None)
1025
1025
  base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
1026
- claims: dict[str, dict[str, Any]] = token_validate(token=token,
1027
- issuer=base_url,
1028
- recipient_id=login_id,
1029
- recipient_attr=recipient_attr,
1030
- public_key=public_key,
1031
- errors=errors,
1032
- logger=logger)
1026
+ claims: dict[str, dict[str, Any]] = jwt_validate(token=token,
1027
+ issuer=base_url,
1028
+ recipient_id=login_id,
1029
+ recipient_attr=recipient_attr,
1030
+ public_key=public_key,
1031
+ errors=errors,
1032
+ logger=logger)
1033
1033
  if claims:
1034
1034
  users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
1035
1035
  errors=errors,
@@ -1,13 +1,12 @@
1
- import requests
2
1
  import sys
3
2
  from datetime import datetime
4
3
  from enum import StrEnum
5
4
  from logging import Logger
6
5
  from pypomes_core import (
7
- APP_PREFIX, TZ_LOCAL, exc_format,
6
+ APP_PREFIX, TZ_LOCAL,
8
7
  env_get_int, env_get_str, env_get_strs
9
8
  )
10
- from pypomes_crypto import crypto_jwk_convert
9
+ from pypomes_crypto import jwt_get_public_key
11
10
  from threading import RLock
12
11
  from typing import Any, Final
13
12
 
@@ -203,7 +202,7 @@ def _iam_server_from_issuer(issuer: str,
203
202
  for iam_server, registry in _IAM_SERVERS.items():
204
203
  base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
205
204
  if base_url == issuer:
206
- result = type(IamServer)(iam_server)
205
+ result = IamServer(iam_server)
207
206
  break
208
207
 
209
208
  if not result:
@@ -222,34 +221,7 @@ def _get_public_key(iam_server: IamServer,
222
221
  """
223
222
  Obtain the public key used by *iam_server* to sign the authentication tokens.
224
223
 
225
- This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
226
- containing the public keys used for various purposes, as indicated in the attribute *use*:
227
- - *enc*: the key is intended for encryption
228
- - *sig*: the key is intended for digital signature
229
- - *wrap*: the key is intended for key wrapping
230
-
231
- A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
232
- {
233
- "keys": [
234
- {
235
- "kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
236
- "kty": "RSA",
237
- "alg": "RSA256",
238
- "use": "sig",
239
- "n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
240
- "e": "AQAB",
241
- "x5c": [
242
- "MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
243
- ],
244
- "x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
245
- "x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
246
- },
247
- ...
248
- ]
249
- }
250
-
251
- Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
252
- to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
224
+ The signaature is obtained and stored in *PEM* (Privacy-Enhanced Mail) format.
253
225
 
254
226
  :param iam_server: the reference registered *IAM* server
255
227
  :param errors: incidental error messages
@@ -265,57 +237,17 @@ def _get_public_key(iam_server: IamServer,
265
237
  if registry:
266
238
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
267
239
  if now > registry[ServerParam.PK_EXPIRATION]:
268
- # obtain the JWKS (JSON Web Key Set) from the token issuer
269
- base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
270
- url: str = f"{base_url}/protocol/openid-connect/certs"
271
- if logger:
272
- logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
273
- logger.debug(msg=f"GET {url}")
274
- try:
275
- response: requests.Response = requests.get(url=url)
276
- if response.status_code == 200:
277
- # request succeeded
278
- if logger:
279
- logger.debug(msg=f"GET success, status {response.status_code}")
280
- # select the appropriate JWK
281
- reply: dict[str, list[dict[str, str]]] = response.json()
282
- jwk: dict[str, str] | None = None
283
- for key in reply["keys"]:
284
- if key.get("use") == "sig":
285
- jwk = key
286
- break
287
- if jwk:
288
- # convert from 'JWK' to 'PEM' and save it for further use
289
- result = crypto_jwk_convert(jwk=jwk,
290
- fmt="PEM")
291
- registry[ServerParam.PUBLIC_KEY] = result
292
- lifetime: int = registry[ServerParam.PK_LIFETIME] or 0
293
- registry[ServerParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
294
- if logger:
295
- logger.debug("Public key obtained and saved")
296
- else:
297
- msg = "Signature public key missing from the token issuer's JWKS"
298
- if logger:
299
- logger.error(msg=msg)
300
- if isinstance(errors, list):
301
- errors.append(msg)
302
- elif logger:
303
- msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
304
- if hasattr(response, "content") and response.content:
305
- msg += f", content {response.content}"
306
- logger.error(msg=msg)
307
- if isinstance(errors, list):
308
- errors.append(msg)
309
- except Exception as e:
310
- # the operation raised an exception
311
- msg = exc_format(exc=e,
312
- exc_info=sys.exc_info())
313
- if logger:
314
- logger.error(msg=msg)
315
- if isinstance(errors, list):
316
- errors.append(msg)
317
- else:
318
- result = registry[ServerParam.PUBLIC_KEY]
240
+ # obtain the public key from the token issuer
241
+ issuer: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
242
+ registry[ServerParam.PUBLIC_KEY] = jwt_get_public_key(issuer=issuer,
243
+ fmt="PEM",
244
+ errors=errors,
245
+ logger=logger)
246
+ lifetime: int = registry[ServerParam.PK_LIFETIME] or 0
247
+ registry[ServerParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
248
+
249
+ if not errors:
250
+ result = registry[ServerParam.PUBLIC_KEY]
319
251
 
320
252
  return result
321
253
 
@@ -131,7 +131,7 @@ def iam_setup_endpoints(flask_app: Flask,
131
131
  if "callback_exchange_endpoint" in defaulted_params:
132
132
  callback_exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK_EXCHANGE")
133
133
  if "exchange_endpoint" in defaulted_params:
134
- callback_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
134
+ exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
135
135
  if "login_endpoint" in defaulted_params:
136
136
  login_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN")
137
137
  if "logout_endpoint" in defaulted_params:
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from flask import Request, Response, request, jsonify
3
+ from pypomes_crypto import jwt_get_values, jwt_validate
3
4
  from logging import Logger
4
5
  from typing import Any
5
6
 
@@ -12,7 +13,6 @@ from .iam_actions import (
12
13
  iam_login, iam_logout, iam_callback,
13
14
  iam_exchange, iam_get_token, iam_userinfo
14
15
  )
15
- from .token_pomes import token_get_claims, token_validate
16
16
 
17
17
  # the logger for IAM service operations
18
18
  # (used exclusively at the HTTP endpoints - all other functions receive the logger as parameter)
@@ -56,36 +56,38 @@ def __request_validate(request: Request) -> Response:
56
56
  bad_token: bool = True
57
57
  token: str = __get_bearer_token(request=request)
58
58
  if token:
59
+ # errors list for error loging, only
60
+ errors: list[str] = []
59
61
  # extract token claims
60
- claims: dict[str, Any] = token_get_claims(token=token)
61
- if claims:
62
- issuer: str = claims["payload"].get("iss")
63
- public_key: str | None = None
64
- recipient_attr: str | None = None
65
- recipient_id: str = request.values.get("user-id") or request.values.get("login")
66
- with _iam_lock:
67
- iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
68
- errors=None,
69
- logger=__IAM_LOGGER)
70
- if iam_server:
71
- # validate the token's recipient only if a user identification is provided
72
- if recipient_id:
73
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
74
- errors=None,
75
- logger=__IAM_LOGGER)
76
- if registry:
77
- recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
78
- public_key = _get_public_key(iam_server=iam_server,
79
- errors=None,
80
- logger=__IAM_LOGGER)
81
- # validate the token (log errors, only)
82
- errors: list[str] = []
83
- if token_validate(token=token,
84
- issuer=issuer,
85
- recipient_id=recipient_id,
86
- recipient_attr=recipient_attr,
87
- public_key=public_key,
88
- errors=errors):
62
+ (issuer,) = jwt_get_values(token=token,
63
+ keys=("iss",))
64
+ public_key: str | None = None
65
+ recipient_attr: str | None = None
66
+ recipient_id: str = (request.values.get("user-id") or request.values.get("login") or
67
+ (request.get_json(silent=True) or {}).get("user-id") or
68
+ (request.get_json(silent=True) or {}).get("login"))
69
+ with _iam_lock:
70
+ iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
71
+ errors=None,
72
+ logger=__IAM_LOGGER)
73
+ if iam_server:
74
+ # validate the token's recipient only if a user identification is provided
75
+ if recipient_id:
76
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
77
+ errors=None,
78
+ logger=__IAM_LOGGER)
79
+ if registry:
80
+ recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
81
+ public_key = _get_public_key(iam_server=iam_server,
82
+ errors=None,
83
+ logger=__IAM_LOGGER)
84
+ # validate the token
85
+ if jwt_validate(token=token,
86
+ issuer=issuer,
87
+ recipient_id=recipient_id,
88
+ recipient_attr=recipient_attr,
89
+ public_key=public_key,
90
+ errors=errors):
89
91
  # token is valid
90
92
  bad_token = False
91
93
  elif __IAM_LOGGER:
@@ -163,7 +165,7 @@ def service_setup_server() -> Response:
163
165
  :return: *Response OK*
164
166
  """
165
167
  # retrieve the request arguments
166
- args: dict[str, Any] = (dict(request.json) if request.is_json else dict(request.form)) or {}
168
+ args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
167
169
 
168
170
  # log the request
169
171
  if __IAM_LOGGER:
@@ -209,7 +211,7 @@ def service_login() -> Response:
209
211
  result: Response | None = None
210
212
 
211
213
  # retrieve the request arguments
212
- args: dict[str, Any] = dict(request.args) or {}
214
+ args: dict[str, Any] = dict(request.args or {})
213
215
 
214
216
  # log the request
215
217
  if __IAM_LOGGER:
@@ -251,7 +253,7 @@ def service_logout() -> Response:
251
253
  the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
252
254
  if the endpoint is established with a call to *iam_setup_endpoints()*.
253
255
 
254
- The user is identified by the attribute *user-id* or "login", provided as a request parameter.
256
+ The user is identified by the attribute *user-id* or "login", provided in the body's *JSON*.
255
257
  If successful, remove all data relating to the user from the *IAM* server's registry.
256
258
  Otherwise, this operation fails silently, unless an error has ocurred.
257
259
 
@@ -261,7 +263,7 @@ def service_logout() -> Response:
261
263
  result: Response | None
262
264
 
263
265
  # retrieve the request arguments
264
- args: dict[str, Any] = dict(request.args) or {}
266
+ args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
265
267
 
266
268
  # log the request
267
269
  if __IAM_LOGGER:
@@ -293,7 +295,7 @@ def service_logout() -> Response:
293
295
 
294
296
 
295
297
  # @flask_app.route(rule=<callback_endpoint>,
296
- # methods=["GET", "POST"])
298
+ # methods=["GET"])
297
299
  def service_callback() -> Response:
298
300
  """
299
301
  Entry point for the callback from the *IAM* server on authentication operation.
@@ -319,7 +321,7 @@ def service_callback() -> Response:
319
321
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
320
322
  """
321
323
  # retrieve the request arguments
322
- args: dict[str, Any] = dict(request.args) or {}
324
+ args: dict[str, Any] = dict(request.args or {})
323
325
 
324
326
  # log the request
325
327
  if __IAM_LOGGER:
@@ -365,7 +367,7 @@ def service_exchange() -> Response:
365
367
  If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
366
368
  Otherwise, *errors* will contain the appropriate error message.
367
369
 
368
- The expected request parameters are:
370
+ The expected request parameters, to be found in the body *JSON*, are:
369
371
  - user-id: identification for the reference user (alias: 'login')
370
372
  - access-token: the token to be exchanged
371
373
 
@@ -378,7 +380,7 @@ def service_exchange() -> Response:
378
380
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
379
381
  """
380
382
  # retrieve the request arguments
381
- args: dict[str, Any] = dict(request.args) or {}
383
+ args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
382
384
 
383
385
  # log the request
384
386
  if __IAM_LOGGER:
@@ -446,7 +448,7 @@ def service_callback_exchange() -> Response:
446
448
  result: Response | None = None
447
449
 
448
450
  # retrieve the request arguments
449
- args: dict[str, Any] = dict(request.args) or {}
451
+ args: dict[str, Any] = dict(request.args or {})
450
452
 
451
453
  # log the request
452
454
  if __IAM_LOGGER:
@@ -1,183 +0,0 @@
1
- import jwt
2
- import sys
3
- from jwt import PyJWK
4
- from jwt.algorithms import RSAPublicKey
5
- from logging import Logger
6
- from pypomes_core import exc_format
7
- from typing import Any
8
-
9
-
10
- def token_get_claims(token: str,
11
- errors: list[str] = None,
12
- logger: Logger = None) -> dict[str, dict[str, Any]] | None:
13
- """
14
- Retrieve the claims set of a JWT *token*.
15
-
16
- Any well-constructed JWT token may be provided in *token*.
17
- Note that neither the token's signature nor its expiration is verified.
18
-
19
- :param token: the refrence token
20
- :param errors: incidental error messages
21
- :param logger: optional logger
22
- :return: the token's claimset, or *None* if error
23
- """
24
- # initialize the return variable
25
- result: dict[str, dict[str, Any]] | None = None
26
-
27
- if logger:
28
- logger.debug(msg="Retrieve claims for token")
29
-
30
- try:
31
- header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
32
- payload: dict[str, Any] = jwt.decode(jwt=token,
33
- options={"verify_signature": False})
34
- result = {
35
- "header": header,
36
- "payload": payload
37
- }
38
- except Exception as e:
39
- exc_err: str = exc_format(exc=e,
40
- exc_info=sys.exc_info())
41
- if logger:
42
- logger.error(msg=f"Error retrieving the token's claims: {exc_err}")
43
- if isinstance(errors, list):
44
- errors.append(exc_err)
45
-
46
- return result
47
-
48
-
49
- def token_get_values(token: str,
50
- keys: tuple[str, ...],
51
- errors: list[str] = None,
52
- logger: Logger = None) -> tuple:
53
- """
54
- Retrieve the values of *keys* in the token's payload.
55
-
56
- Ther values are returned in the same order as requested in *keys*.
57
- For a claim not found, *None* is returned in its position.
58
-
59
- :param token: the reference token
60
- :param keys: the names of the claims whose values are to be returned
61
- :param errors: incidental errors
62
- :param logger: optiona logger
63
- :return: a tuple containing the respective values of *claims* in *token*.
64
- """
65
- token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
66
- errors=errors,
67
- logger=logger)
68
- payload: dict[str, Any] = token_claims["payload"]
69
- values: list[Any] = []
70
- for key in keys:
71
- values.append(payload.get(key))
72
-
73
- return tuple(values)
74
-
75
-
76
- def token_validate(token: str,
77
- issuer: str = None,
78
- recipient_id: str = None,
79
- recipient_attr: str = None,
80
- public_key: str | bytes | PyJWK | RSAPublicKey = None,
81
- errors: list[str] = None,
82
- logger: Logger = None) -> dict[str, dict[str, Any]] | None:
83
- """
84
- Verify whether *token* is a valid JWT token, and return its claims (sections *header* and *payload*).
85
-
86
- The supported public key types are:
87
- - *DER*: Distinguished Encoding Rules (bytes)
88
- - *PEM*: Privacy-Enhanced Mail (str)
89
- - *PyJWK*: a formar from the *PyJWT* package
90
- - *RSAPublicKey*: a format from the *PyJWT* package
91
-
92
- If an asymmetric algorithm was used to sign the token and *public_key* is provided, then
93
- the token is validated, by using the data in its *signature* section.
94
-
95
- The parameters *recipient_id* and *recipient_attr* refer the token's expected subject, respectively,
96
- the subject's identification and the attribute in the token's payload data identifying its subject.
97
- If both are provided, *recipient_id* is validated.
98
-
99
- On failure, *errors* will contain the reason(s) for rejecting *token*.
100
- On success, return the token's claims (*header* and *payload*).
101
-
102
- :param token: the token to be validated
103
- :param public_key: optional public key used to sign the token, in *PEM* format
104
- :param issuer: optional value to compare with the token's *iss* (issuer) attribute in its *payload*
105
- :param recipient_id: identification of the expected token subject
106
- :param recipient_attr: attribute in the token's payload holding the expected subject's identification
107
- :param errors: incidental error messages
108
- :param logger: optional logger
109
- :return: The token's claims (*header* and *payload*), or *None* if error
110
- """
111
- # initialize the return variable
112
- result: dict[str, dict[str, Any]] | None = None
113
-
114
- if logger:
115
- logger.debug(msg="Validate JWT token")
116
-
117
- # make sure to have an errors list
118
- if not isinstance(errors, list):
119
- errors = []
120
-
121
- # extract needed data from token header
122
- token_header: dict[str, Any] | None = None
123
- try:
124
- token_header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
125
- except Exception as e:
126
- exc_err: str = exc_format(exc=e,
127
- exc_info=sys.exc_info())
128
- if logger:
129
- logger.error(msg=f"Error retrieving the token's header: {exc_err}")
130
- errors.append(exc_err)
131
-
132
- # validate the token
133
- if not errors:
134
- token_alg: str = token_header.get("alg")
135
- require: list[str] = ["exp", "iat"]
136
- if issuer:
137
- require.append("iss")
138
- options: dict[str, Any] = {
139
- "require": require,
140
- "verify_aud": False,
141
- "verify_exp": True,
142
- "verify_iat": True,
143
- "verify_iss": issuer is not None,
144
- "verify_nbf": False,
145
- "verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
146
- }
147
- try:
148
- # raises:
149
- # InvalidTokenError: token is invalid
150
- # InvalidKeyError: authentication key is not in the proper format
151
- # ExpiredSignatureError: token and refresh period have expired
152
- # InvalidSignatureError: signature does not match the one provided as part of the token
153
- # ImmatureSignatureError: 'nbf' or 'iat' claim represents a timestamp in the future
154
- # InvalidAlgorithmError: the specified algorithm is not recognized
155
- # InvalidIssuedAtError: 'iat' claim is non-numeric
156
- # MissingRequiredClaimError: a required claim is not contained in the claimset
157
- payload: dict[str, Any] = jwt.decode(jwt=token,
158
- key=public_key,
159
- algorithms=[token_alg],
160
- options=options,
161
- issuer=issuer)
162
- if recipient_id and recipient_attr and \
163
- payload.get(recipient_attr) and recipient_id != payload.get(recipient_attr):
164
- msg: str = f"Token was issued to '{payload.get(recipient_attr)}', not to '{recipient_id}'"
165
- if logger:
166
- logger.error(msg=msg)
167
- errors.append(msg)
168
- else:
169
- result = {
170
- "header": token_header,
171
- "payload": payload
172
- }
173
- except Exception as e:
174
- exc_err: str = exc_format(exc=e,
175
- exc_info=sys.exc_info())
176
- if logger:
177
- logger.error(msg=f"Error decoding the token: {exc_err}")
178
- errors.append(exc_err)
179
-
180
- if not errors and logger:
181
- logger.debug(msg="Token is valid")
182
-
183
- return result
File without changes
File without changes
File without changes
File without changes