Ce module Python fournit une implémentation bas niveau. Il ne dépend pas de bibliothèques haut niveau comme PyJWT : on maîtrise entièrement le processus.
Comment cela fonctionne (étape par étape)
1) Découpage du JWT
Le token doit avoir la forme :
header.payload.signature
Le code vérifie :
qu’il y a bien 3 segments,
qu’ils sont valides en Base64URL,
qu’ils contiennent du JSON correct.
En cas de problème → `JWTFormatError`.
2) Décodage Base64URL
Chaque segment est décodé en bytes, avec ajout automatique du padding `=` si nécessaire.
En cas d’erreur → `JWTFormatError`.
3) Analyse du header
Le header doit contenir un champ :
"alg" : "HS256" | "RS256" | ...
Si l’algorithme est absent ou non autorisé → `JWTAlgorithmError`.
4) Vérification de la signature
Selon l’algorithme :
HMAC (HS256 / HS384 / HS512)
Recalcul du HMAC avec la clé secrète
Comparaison en temps constant (Time Constant Comparison) : `compare_digest`
→ protège contre les attaques par timing
Si la signature ne correspond pas → `JWTSignatureError`.
RSA (RS256 / RS384 / RS512)
Chargement de la clé publique PEM
Vérification via `cryptography` + PKCS#1 v1.5
Si la signature est invalide → `JWTSignatureError`.
5) Retour du payload
Si tout est correct, la fonction renvoie le **payload décodé** (dict Python).
Voici le code :
- # jwt_validation.py
- # coding: utf8
- """
- Décodage et validation d'un Jeton d'Identité JSON Web Token (JWT).
- Auteur :
- B.Degoy bertrand@degoy.com
- copyright (c) 2026 B.Degoy
- licence : MIT
- """
- import json
- import base64
- import hmac
- import hashlib
- from typing import Any, Dict
- class JWTDecodeError(Exception):
- """Base class for all JWT decoding errors."""
- pass
- class JWTFormatError(JWTDecodeError):
- """Raised when the JWT structure is invalid."""
- pass
- class JWTAlgorithmError(JWTDecodeError):
- """Raised when the algorithm is missing or not allowed."""
- pass
- class JWTSignatureError(JWTDecodeError):
- """Raised when the signature is invalid."""
- pass
- class JWTUtils:
- """
- A static utility class providing low-level JWT decoding and signature
- verification, equivalent to the provided PHP implementation but using
- Python exceptions instead of returning False.
- This class does NOT validate claims (exp, nbf, iss, etc.).
- """
- # ----------------------------------------------------------------------
- # Base64URL decoding
- # ----------------------------------------------------------------------
- @staticmethod
- def urlsafe_b64decode(data: str) -> bytes:
- """
- Decode a Base64URL string into raw bytes.
- Adds required padding if missing.
- """
- padding = "=" * (-len(data) % 4)
- try:
- return base64.urlsafe_b64decode(data + padding)
- except Exception as e:
- raise JWTFormatError(f"Invalid Base64URL segment: {data}") from e
- # ----------------------------------------------------------------------
- # HMAC signing
- # ----------------------------------------------------------------------
- @staticmethod
- def sign(message: str, key: bytes, algo: str) -> bytes:
- """
- Compute an HMAC signature for the given message using HS256/384/512.
- """
- if algo == "HS256":
- return hmac.new(key, message.encode(), hashlib.sha256).digest()
- if algo == "HS384":
- return hmac.new(key, message.encode(), hashlib.sha384).digest()
- if algo == "HS512":
- return hmac.new(key, message.encode(), hashlib.sha512).digest()
- raise JWTAlgorithmError(f"Unsupported HMAC algorithm: {algo}")
- # ----------------------------------------------------------------------
- # Constant-time comparison
- # ----------------------------------------------------------------------
- @staticmethod
- def hash_equals(a: bytes, b: bytes) -> bool:
- """Constant-time comparison to avoid timing attacks."""
- return hmac.compare_digest(a, b)
- # ----------------------------------------------------------------------
- # RSA signature verification
- # ----------------------------------------------------------------------
- @staticmethod
- def verify_rsa(signature: bytes, message: str, key_pem: str, algo: str) -> bool:
- """
- Verify an RSA signature using a PEM-encoded public key.
- Supported algorithms: RS256, RS384, RS512.
- """
- from cryptography.hazmat.primitives import serialization, hashes
- from cryptography.hazmat.primitives.asymmetric import padding
- public_key = serialization.load_pem_public_key(key_pem.encode())
- hash_algo = {
- "RS256": hashes.SHA256(),
- "RS384": hashes.SHA384(),
- "RS512": hashes.SHA512(),
- }.get(algo)
- if hash_algo is None:
- raise JWTAlgorithmError(f"Unsupported RSA algorithm: {algo}")
- try:
- public_key.verify(
- signature,
- message.encode(),
- padding.PKCS1v15(),
- hash_algo
- )
- return True
- except Exception:
- return False
- # ----------------------------------------------------------------------
- # Signature verification dispatcher
- # ----------------------------------------------------------------------
- @staticmethod
- def verify_signature(signature: bytes, input_data: str, key: Any, algo: str) -> None:
- """
- Verify the signature for the given algorithm.
- Raises JWTSignatureError on failure.
- """
- if algo in ("HS256", "HS384", "HS512"):
- key_bytes = key if isinstance(key, bytes) else key.encode()
- expected = JWTUtils.sign(input_data, key_bytes, algo)
- if not JWTUtils.hash_equals(expected, signature):
- raise JWTSignatureError("Invalid HMAC signature")
- return
- if algo in ("RS256", "RS384", "RS512"):
- if not JWTUtils.verify_rsa(signature, input_data, key, algo):
- raise JWTSignatureError("Invalid RSA signature")
- return
- raise JWTAlgorithmError(f"Unsupported or invalid signing algorithm: {algo}")
- # ----------------------------------------------------------------------
- # Main decode function
- # ----------------------------------------------------------------------
- @staticmethod
- def decode(jwt: str, key: Any = None, allowed_algorithms: Any = True) -> Dict:
- """
- Decode a JWT token, validate its structure, optionally verify its
- signature, and return the payload as a dictionary.
- Raises:
- JWTFormatError: Invalid structure or Base64URL segments.
- JWTAlgorithmError: Missing or disallowed algorithm.
- JWTSignatureError: Signature verification failed.
- """
- # Basic structure check
- if "." not in jwt:
- raise JWTFormatError("JWT must contain at least one dot")
- parts = jwt.split(".")
- if len(parts) != 3:
- raise JWTFormatError("JWT must contain exactly 3 segments")
- head_b64, payload_b64, sig_b64 = parts
- # Decode JSON header and payload
- try:
- header = json.loads(JWTUtils.urlsafe_b64decode(head_b64))
- except Exception as e:
- raise JWTFormatError("Invalid JWT header") from e
- try:
- payload = json.loads(JWTUtils.urlsafe_b64decode(payload_b64))
- except Exception as e:
- raise JWTFormatError("Invalid JWT payload") from e
- signature = JWTUtils.urlsafe_b64decode(sig_b64)
- # Algorithm checks
- if allowed_algorithms:
- if "alg" not in header:
- raise JWTAlgorithmError("Missing 'alg' in JWT header")
- algo = header["alg"]
- if isinstance(allowed_algorithms, list) and algo not in allowed_algorithms:
- raise JWTAlgorithmError(f"Algorithm '{algo}' not allowed")
- signing_input = f"{head_b64}.{payload_b64}"
- JWTUtils.verify_signature(signature, signing_input, key, algo)
- return payload
Validation des déclarations (claims)
L’utilitaire ci-dessus décode le JWT et vérifie la signature — mais **il ne valide pas les claims**, c’est‑à‑dire les champs qui définissent *quand* et *dans quelles conditions* le token est valable.
Quels claims doivent être validés ?
Les principaux champs standardisés dans un JWT sont :
| Claim | Signification | Validation |
| `exp` | Expiration time | Le token doit être encore valide |
| `nbf` | Not Before | Le token ne doit pas être utilisé trop tôt |
| `iat` | Issued At | Optionnel : vérifier que la date n’est pas aberrante |
| `iss` | Issuer | Vérifier que le token vient du bon émetteur |
| `aud` | Audience | Vérifier que le token est destiné à ton service |
| `sub` | Subject | Optionnel : vérifier l’identité attendue |
Fonction validate_claims()
Voici comment ajouter cette validation :
On ajoute une fonction **validate_claims(payload, options)** qui :
lit les champs du payload,
compare avec l’heure actuelle,
lève des exceptions explicites si quelque chose ne va pas.
- from datetime import datetime, timezone
- class JWTClaimsError(Exception):
- """Raised when JWT claims validation fails."""
- pass
- class JWTUtils:
- # ... (tout le reste de ta classe ici)
- @staticmethod
- def validate_claims(
- payload: dict,
- issuer: str = None,
- audience: str = None,
- leeway: int = 0
- ) -> None:
- """
- Validate standard JWT claims (exp, nbf, iss, aud).
- Parameters:
- payload (dict): The decoded JWT payload.
- issuer (str|None): Expected 'iss' value.
- audience (str|None): Expected 'aud' value.
- leeway (int): Allowed clock skew in seconds.
- Raises:
- JWTClaimsError: If any claim is invalid.
- """
- now = datetime.now(timezone.utc).timestamp()
- # --- exp: expiration time ---
- if "exp" in payload:
- if now > payload["exp"] + leeway:
- raise JWTClaimsError("Token has expired")
- # --- nbf: not before ---
- if "nbf" in payload:
- if now < payload["nbf"] - leeway:
- raise JWTClaimsError("Token is not yet valid (nbf)")
- # --- iat: issued at ---
- if "iat" in payload:
- if payload["iat"] > now + leeway:
- raise JWTClaimsError("Token issued in the future (iat)")
- # --- iss: issuer ---
- if issuer is not None:
- if payload.get("iss") != issuer:
- raise JWTClaimsError(f"Invalid issuer: {payload.get('iss')}")
- # --- aud: audience ---
- if audience is not None:
- aud = payload.get("aud")
- if isinstance(aud, list):
- if audience not in aud:
- raise JWTClaimsError(f"Audience '{audience}' not allowed")
- else:
- if aud != audience:
- raise JWTClaimsError(f"Invalid audience: {aud}")
- </python>
- {{intégration dans `decode()`}}
- On ajoute un paramètre optionnel :
- <code class="python">
- @staticmethod
- def decode(jwt, key=None, allowed_algorithms=True, validate=False, issuer=None, audience=None):
- payload = ... # comme avant
- if validate:
- JWTUtils.validate_claims(payload, issuer=issuer, audience=audience)
- return payload
- </python>
- Et on l’appelle comme ceci :
- <code class="python">
- payload = JWTUtils.decode(
- token,
- key=public_key,
- allowed_algorithms=["RS256"],
- validate=True,
- issuer="https://auth.example.com",
- audience="my-api"
- )
