pypomes-jwt 1.3.8__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.
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/PKG-INFO +3 -2
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/pyproject.toml +3 -2
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/src/pypomes_jwt/__init__.py +2 -2
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/src/pypomes_jwt/jwt_pomes.py +122 -14
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/.gitignore +0 -0
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/LICENSE +0 -0
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/README.md +0 -0
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/src/pypomes_jwt/jwt_config.py +0 -0
- {pypomes_jwt-1.3.8 → pypomes_jwt-1.4.0}/src/pypomes_jwt/jwt_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_jwt
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -52,7 +53,7 @@ def jwt_verify_request(request: Request) -> Response:
|
|
|
52
53
|
# validate the authorization token
|
|
53
54
|
bad_token: bool = True
|
|
54
55
|
if auth_header and auth_header.startswith("Bearer "):
|
|
55
|
-
#
|
|
56
|
+
# extract and validate the JWT access token
|
|
56
57
|
token: str = auth_header.split(" ")[1]
|
|
57
58
|
claims: dict[str, Any] = jwt_validate_token(token=token,
|
|
58
59
|
nature="A")
|
|
@@ -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
|
|
|
@@ -224,14 +226,17 @@ def jwt_validate_token(token: str,
|
|
|
224
226
|
# InvalidIssuedAtError: 'iat' claim is non-numeric
|
|
225
227
|
# MissingRequiredClaimError: a required claim is not contained in the claimset
|
|
226
228
|
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
229
|
+
key=token_decoder,
|
|
230
|
+
algorithms=token_alg,
|
|
227
231
|
options={
|
|
228
|
-
"
|
|
232
|
+
"require": ["iat", "iss", "exp", "sub"],
|
|
233
|
+
"verify_aud": False,
|
|
229
234
|
"verify_exp": True,
|
|
230
|
-
"
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
"verify_iat": True,
|
|
236
|
+
"verify_iss": False,
|
|
237
|
+
"verify_nbf": True,
|
|
238
|
+
"verify_signature": True
|
|
239
|
+
})
|
|
235
240
|
if account_id and payload.get("sub") != account_id:
|
|
236
241
|
if logger:
|
|
237
242
|
logger.error(msg=f"Token does not belong to account '{account_id}'")
|
|
@@ -505,7 +510,7 @@ def jwt_refresh_tokens(account_id: str,
|
|
|
505
510
|
|
|
506
511
|
def jwt_get_claims(token: str,
|
|
507
512
|
errors: list[str] = None,
|
|
508
|
-
logger: Logger = None) -> dict[str, Any] | None:
|
|
513
|
+
logger: Logger = None) -> dict[str, dict[str, Any]] | None:
|
|
509
514
|
"""
|
|
510
515
|
Retrieve the claims set of a JWT *token*.
|
|
511
516
|
|
|
@@ -520,8 +525,6 @@ def jwt_get_claims(token: str,
|
|
|
520
525
|
"kid": "A1234"
|
|
521
526
|
},
|
|
522
527
|
"payload": {
|
|
523
|
-
"valid-from": <YYYY-MM-DDThh:mm:ss+00:00>
|
|
524
|
-
"valid-until": <YYYY-MM-DDThh:mm:ss+00:00>
|
|
525
528
|
"birthdate": "1980-01-01",
|
|
526
529
|
"email": "jdoe@mail.com",
|
|
527
530
|
"exp": 1516640454,
|
|
@@ -539,13 +542,13 @@ def jwt_get_claims(token: str,
|
|
|
539
542
|
}
|
|
540
543
|
}
|
|
541
544
|
|
|
542
|
-
:param token: the token
|
|
545
|
+
:param token: the reference token
|
|
543
546
|
:param errors: incidental error messages
|
|
544
547
|
:param logger: optional logger
|
|
545
548
|
:return: the token's claimset, or *None* if error
|
|
546
549
|
"""
|
|
547
550
|
# initialize the return variable
|
|
548
|
-
result: dict[str, Any] | None = None
|
|
551
|
+
result: dict[str, dict[str, Any]] | None = None
|
|
549
552
|
|
|
550
553
|
if logger:
|
|
551
554
|
logger.debug(msg="Retrieve claims for token")
|
|
@@ -562,8 +565,113 @@ def jwt_get_claims(token: str,
|
|
|
562
565
|
exc_err: str = exc_format(exc=e,
|
|
563
566
|
exc_info=sys.exc_info())
|
|
564
567
|
if logger:
|
|
565
|
-
logger.error(msg=f"Error retrieving the token's
|
|
568
|
+
logger.error(msg=f"Error retrieving the token's claims: {exc_err}")
|
|
566
569
|
if isinstance(errors, list):
|
|
567
570
|
errors.append(exc_err)
|
|
568
571
|
|
|
569
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
|
|
File without changes
|
|
File without changes
|