pypomes-jwt 1.3.8__py3-none-any.whl → 1.4.0__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.

Potentially problematic release.


This version of pypomes-jwt might be problematic. Click here for more details.

pypomes_jwt/__init__.py CHANGED
@@ -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"
pypomes_jwt/jwt_pomes.py CHANGED
@@ -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
- # yes, extract and validate the JWT access token
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
- "verify_signature": True,
232
+ "require": ["iat", "iss", "exp", "sub"],
233
+ "verify_aud": False,
229
234
  "verify_exp": True,
230
- "verify_nbf": True
231
- },
232
- key=token_decoder,
233
- require=["iat", "iss", "exp", "sub"],
234
- algorithms=token_alg)
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 to be inspected for claims
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 claimsn: {exc_err}")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_jwt
3
- Version: 1.3.8
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
@@ -0,0 +1,8 @@
1
+ pypomes_jwt/__init__.py,sha256=VvDSmB6PINjpsYxSgg302dwpKAC0sZjV-2iZr1Oc6uo,862
2
+ pypomes_jwt/jwt_config.py,sha256=ypr7BCRp1slJ503iyVmma-ljbaZAnbk_qpZKNRjD5CI,4026
3
+ pypomes_jwt/jwt_pomes.py,sha256=IDcipM0ckFpoJOz2dbcTbp46gLKrQ1tfvP2ErWTXceA,28464
4
+ pypomes_jwt/jwt_registry.py,sha256=ypBEoL0I2F08sR2G2VO9wXxVeE252lNzjIAC3FGORhA,22631
5
+ pypomes_jwt-1.4.0.dist-info/METADATA,sha256=H7mM6PK66nfdOFxShbslmmUDzB_MmFj4f1LMsfjw_x8,697
6
+ pypomes_jwt-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ pypomes_jwt-1.4.0.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
+ pypomes_jwt-1.4.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pypomes_jwt/__init__.py,sha256=esLvNt3Vr4WiZlx1lqKbLIXpDhdBFjqhqKUM6laSwg4,820
2
- pypomes_jwt/jwt_config.py,sha256=ypr7BCRp1slJ503iyVmma-ljbaZAnbk_qpZKNRjD5CI,4026
3
- pypomes_jwt/jwt_pomes.py,sha256=WWmZYVpG6wqlIjcJU3jNyBquCgfB-OcgCJBmm6imL4Q,23524
4
- pypomes_jwt/jwt_registry.py,sha256=ypBEoL0I2F08sR2G2VO9wXxVeE252lNzjIAC3FGORhA,22631
5
- pypomes_jwt-1.3.8.dist-info/METADATA,sha256=8qS7vQRszQmdG5YU2bWTzlFIy_XpoaEcruzNL40TDec,660
6
- pypomes_jwt-1.3.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- pypomes_jwt-1.3.8.dist-info/licenses/LICENSE,sha256=NdakochSXm_H_-DSL_x2JlRCkYikj3snYYvTwgR5d_c,1086
8
- pypomes_jwt-1.3.8.dist-info/RECORD,,