pypomes-jwt 1.3.9__tar.gz → 1.4.0__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-jwt might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 1.3.9
3
+ Version: 1.4.0
4
4
  Summary: A collection of Python pomes, penyeach (JWT module)
5
5
  Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-JWT
6
6
  Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-JWT/issues
@@ -13,5 +13,6 @@ Requires-Python: >=3.12
13
13
  Requires-Dist: cryptography>=46.0.3
14
14
  Requires-Dist: flask>=3.1.2
15
15
  Requires-Dist: pyjwt>=2.10.1
16
- Requires-Dist: pypomes-core>=2.7.8
16
+ Requires-Dist: pypomes-core>=2.8.1
17
+ Requires-Dist: pypomes-crypto>=0.4.8
17
18
  Requires-Dist: pypomes-db>=2.8.1
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_jwt"
9
- version = "1.3.9"
9
+ version = "1.4.0"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -22,7 +22,8 @@ dependencies = [
22
22
  "cryptography>=46.0.3",
23
23
  "Flask>=3.1.2",
24
24
  "PyJWT>=2.10.1",
25
- "pypomes_core>=2.7.8",
25
+ "pypomes_core>=2.8.1",
26
+ "pypomes_crypto>=0.4.8",
26
27
  "pypomes_db>=2.8.1"
27
28
  ]
28
29
 
@@ -2,7 +2,7 @@ from .jwt_config import (
2
2
  JwtConfig, JwtDbConfig, JwtAlgorithm
3
3
  )
4
4
  from .jwt_pomes import (
5
- jwt_needed, jwt_verify_request,
5
+ jwt_needed, jwt_verify_request, jwt_get_public_key,
6
6
  jwt_assert_account, jwt_set_account, jwt_remove_account,
7
7
  jwt_issue_token, jwt_issue_tokens, jwt_refresh_tokens,
8
8
  jwt_get_claims, jwt_validate_token, jwt_revoke_token
@@ -12,7 +12,7 @@ __all__ = [
12
12
  # jwt_config
13
13
  "JwtConfig", "JwtDbConfig", "JwtAlgorithm",
14
14
  # jwt_pomes
15
- "jwt_needed", "jwt_verify_request",
15
+ "jwt_needed", "jwt_verify_request", "jwt_get_public_key",
16
16
  "jwt_assert_account", "jwt_set_account", "jwt_remove_account",
17
17
  "jwt_issue_token", "jwt_issue_tokens", "jwt_refresh_tokens",
18
18
  "jwt_get_claims", "jwt_validate_token", "jwt_revoke_token"
@@ -1,4 +1,5 @@
1
1
  import jwt
2
+ import requests
2
3
  import sys
3
4
  from base64 import b64decode
4
5
  from flask import Request, Response, request
@@ -8,7 +9,7 @@ from pypomes_db import (
8
9
  DbEngine, db_connect, db_commit,
9
10
  db_rollback, db_close, db_select, db_delete
10
11
  )
11
- from typing import Any
12
+ from typing import Any, Literal
12
13
 
13
14
  from .jwt_config import JwtConfig, JwtDbConfig
14
15
  from .jwt_registry import JwtRegistry
@@ -143,6 +144,7 @@ def jwt_validate_token(token: str,
143
144
  then the cryptographic key needed for validation will be obtained from the token database.
144
145
  Otherwise, the current decoding key is used.
145
146
 
147
+ Validation operations require access to a database table defined by *JWT_DB_TABLE*.
146
148
  On success, return the token's claims (*header* and *payload*), as documented in *jwt_get_claims()*
147
149
  On failure, *errors* will contain the reason(s) for rejecting *token*.
148
150
 
@@ -568,3 +570,108 @@ def jwt_get_claims(token: str,
568
570
  errors.append(exc_err)
569
571
 
570
572
  return result
573
+
574
+
575
+ def jwt_get_public_key(token: str,
576
+ fmt: Literal["DER", "PEM"] = None,
577
+ errors: list[str] = None,
578
+ logger: Logger = None) -> dict[str, str] | str | None:
579
+ """
580
+ Obtain the public key used *token*.
581
+
582
+ This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
583
+ containing the public keys used for various purposes, as indicated in the attribute *use*:
584
+ - *enc*: the key is intended for encryption
585
+ - *sig*: the key is intended for digital signature
586
+ - *wrap*: the key is intended for key wrapping
587
+
588
+ A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
589
+ {
590
+ "keys": [
591
+ {
592
+ "kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
593
+ "kty": "RSA",
594
+ "alg": "RSA256",
595
+ "use": "sig",
596
+ "n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
597
+ "e": "AQAB",
598
+ "x5c": [
599
+ "MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
600
+ ],
601
+ "x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
602
+ "x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
603
+ },
604
+ ...
605
+ ]
606
+ }
607
+
608
+ The signature key is returned in its original *JWK* (JSON Web Key) format, or converted to
609
+ either *DER* (Distinguished Encoding Rules) or *PEM* (Privacy-Enhanced Mail) format, as per *ftm*.
610
+
611
+ :param token: the reference token
612
+ :param fmt: the returning key's format
613
+ :param errors: incidental error messages
614
+ :param logger: optional logger
615
+ :return: the public key in *JWT*, *DER*, or *PEM* format, or *None* if error
616
+ """
617
+ from pypomes_crypto import crypto_jwk_convert
618
+
619
+ # initialize the return variable
620
+ result: dict[str, str] | str | None = None
621
+
622
+ claims: dict[str, Any] = jwt_get_claims(token=token,
623
+ errors=errors,
624
+ logger=logger)
625
+ if not errors:
626
+ # obtain the JWKS (JSON Web Key Set) from the token issuer
627
+ issuer: str = claims["payload"].get("iss")
628
+ url: str = f"{issuer}/protocol/openid-connect/certs"
629
+ if logger:
630
+ logger.debug(msg=f"GET {url}")
631
+ try:
632
+ response: requests.Response = requests.get(url=url)
633
+ if response.status_code == 200:
634
+ # request succeeded
635
+ if logger:
636
+ logger.debug(msg=f"GET success, status {response.status_code}")
637
+ # select the appropriate JWK
638
+ reply: dict[str, list[dict[str, str]]] = response.json()
639
+ jwk: dict[str, str] | None = None
640
+ for key in reply["keys"]:
641
+ if key.get("use") == "sig":
642
+ jwk = key
643
+ break
644
+ if jwk:
645
+ # convert from 'JWK' to 'PEM' and save it for further use
646
+ if fmt in ["DER", "PEM"]:
647
+ # noinspection PyTypeChecker
648
+ result = crypto_jwk_convert(jwk=jwk,
649
+ fmt=fmt)
650
+ else:
651
+ result = jwk
652
+ if fmt and logger:
653
+ logger.debug(f"Public key obtained for isuer '{issuer}'")
654
+ else:
655
+ msg: str = (f"Signature public key missing from the JWKS "
656
+ f"returned by the token issuer '{issuer}'")
657
+ if logger:
658
+ logger.error(msg=msg)
659
+ if isinstance(errors, list):
660
+ errors.append(msg)
661
+ elif logger:
662
+ msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
663
+ if hasattr(response, "content") and response.content:
664
+ msg += f", content {response.content}"
665
+ logger.error(msg=msg)
666
+ if isinstance(errors, list):
667
+ errors.append(msg)
668
+ except Exception as e:
669
+ # the operation raised an exception
670
+ msg = exc_format(exc=e,
671
+ exc_info=sys.exc_info())
672
+ if logger:
673
+ logger.error(msg=msg)
674
+ if isinstance(errors, list):
675
+ errors.append(msg)
676
+
677
+ return result
File without changes
File without changes
File without changes