
Génération et validation des tokens JWT
Maîtrisez les processus de génération et de validation des JSON Web Tokens (JWT) pour sécuriser vos applications Spring Boot. Détails sur les claims, signatures et bibliothèques.
Comprendre le processus : de la création à la vérification
La sécurisation stateless des API via JWT repose sur deux opérations fondamentales : la génération d'un token après une authentification réussie et la validation de ce token à chaque requête subséquente sur une ressource protégée. Ces deux étapes sont cruciales et doivent être implémentées avec soin pour garantir la sécurité.
La génération consiste à créer une chaîne de caractères JWT signée, contenant des informations (claims) sur l'utilisateur et sa session (notamment la date d'expiration). La validation, à l'inverse, consiste à vérifier l'authenticité, l'intégrité et la validité temporelle d'un token reçu, puis à extraire les informations qu'il contient pour identifier et autoriser l'utilisateur.
Nous allons détailler ces deux processus en nous appuyant sur la bibliothèque populaire Java JWT (jjwt), couramment utilisée dans l'écosystème Spring Boot pour manipuler les JWTs.
Etape 1 : Génération d'un token JWT
La génération d'un JWT intervient typiquement après qu'un utilisateur a prouvé son identité (par exemple, via login/mot de passe). Le serveur crée alors un token pour les requêtes futures.
Les éléments clés de la génération :
- Choix de l'algorithme de signature : Définit comment la signature sera calculée. Les plus courants sont HMAC avec SHA-256 (HS256), SHA-384 (HS384), SHA-512 (HS512) qui utilisent un secret partagé, ou RSA avec SHA-256 (RS256), etc., qui utilisent une paire de clés publique/privée.
- La clé secrète (ou privée) : C'est l'élément critique pour la sécurité. Pour HMAC, c'est une chaîne de caractères secrète partagée entre le générateur et le validateur. Pour RSA/ECDSA, c'est la clé privée utilisée pour signer. Elle doit être complexe et stockée de manière sécurisée (jamais en dur dans le code). Avec `jjwt`, on peut générer ou représenter ces clés :
// Pour HMAC (génère une clé ou utilise une chaîne secrète existante encodée) SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // Génère une nouvelle clé sécurisée // Ou à partir d'un secret existant (doit être assez long et encodé en Base64) String base64Secret = "VotreSecretTresLongEtComplexeEncodeEnBase64..."; byte[] keyBytes = Decoders.BASE64.decode(base64Secret); SecretKey keyFromSecret = Keys.hmacShaKeyFor(keyBytes); // Pour RSA (génère une paire ou charge depuis un keystore/fichier) KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); PrivateKey privateKey = keyPair.getPrivate(); PublicKey publicKey = keyPair.getPublic(); - Définition des Claims (Revendications) : Ce sont les informations contenues dans le payload. On distingue :
- Claims enregistrés (standard) : `iss` (Issuer - émetteur), `sub` (Subject - sujet, souvent l'ID ou username de l'utilisateur), `aud` (Audience - destinataire), `exp` (Expiration Time - date d'expiration), `nbf` (Not Before - date avant laquelle le token n'est pas valide), `iat` (Issued At - date d'émission), `jti` (JWT ID - identifiant unique).
- Claims personnalisés : Toutes les informations supplémentaires que vous souhaitez inclure, comme les rôles, les permissions, etc. (ex: `roles`, `permissions`).
- Construction et signature : La bibliothèque `jjwt` fournit un builder fluide pour assembler le tout.
Exemple de code de génération avec `jjwt` et HS256 :
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.io.Decoders;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.List;
import java.util.Map;
// ... (dans un service @Service, avec clé et expiration injectées via @Value)
public String createToken(String username, List roles, long expirationInMillis) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expirationInMillis);
// Récupération de la clé (supposons qu'elle soit stockée de manière sécurisée)
String base64Secret = "..."; // Votre secret encodé
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
return Jwts.builder()
.setSubject(username) // Le sujet du token (l'utilisateur)
.claim("roles", roles) // Ajout d'un claim personnalisé pour les rôles
//.claim("autreInfo", "valeur") // Autres claims si nécessaire
.setIssuedAt(now) // Date d'émission
.setExpiration(expiryDate) // Date d'expiration (TRES IMPORTANT)
//.setIssuer("MonApplication") // Optionnel : émetteur
//.setAudience("MonAPI") // Optionnel : audience
.signWith(key, SignatureAlgorithm.HS256) // Signature avec la clé et l'algo HS256
.compact(); // Construit le JWT sous forme de chaîne
}
Etape 2 : Validation d'un token JWT
Lorsqu'une requête arrive avec un token JWT (typiquement dans l'en-tête `Authorization: Bearer
Le processus de validation implique plusieurs vérifications :
- Intégrité et Authenticité (Vérification de la signature) : Le serveur recalcule la signature en utilisant l'en-tête, le payload reçus et la même clé secrète (pour HMAC) ou la clé publique correspondante (pour RSA/ECDSA). Si la signature recalculée correspond à celle fournie dans le token, cela prouve que le token n'a pas été modifié et qu'il a bien été émis par une partie possédant la clé secrète/privée.
- Validité Temporelle (Vérification de l'expiration) : Le serveur vérifie le claim `exp`. Si la date actuelle est postérieure à la date d'expiration, le token est invalide. Il peut aussi vérifier le claim `nbf` (Not Before) si présent.
- Autres Claims (Optionnel mais recommandé) : Le serveur peut vérifier d'autres claims comme l'émetteur (`iss`) ou l'audience (`aud`) s'ils sont utilisés, pour s'assurer que le token provient de la bonne source et est destiné à la bonne application.
Validation avec `jjwt` : La bibliothèque simplifie grandement ce processus. La méthode `parseClaimsJws(token)` effectue automatiquement la vérification de la signature et de l'expiration (ainsi que `nbf` si présent). Elle lève des exceptions spécifiques en cas d'échec.
Gestion des erreurs de validation : Il est crucial de capturer les exceptions spécifiques levées par `jjwt` pour identifier la cause de l'échec :
- `ExpiredJwtException` : Le token a expiré.
- `UnsupportedJwtException` : Le format ou l'algorithme du token n'est pas supporté.
- `MalformedJwtException` : Le token est malformé (structure invalide).
- `SignatureException` : La signature est invalide (token modifié ou mauvaise clé utilisée pour la validation).
- `IllegalArgumentException` : Argument invalide (par exemple, token null ou vide).
Exemple de code de validation avec `jjwt` et HS256 :
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// ... (dans le même service ou un composant dédié)
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
public boolean isTokenValid(String token) {
try {
// Récupération de la clé (la même que pour la génération)
String base64Secret = "...";
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
Jwts.parserBuilder()
.setSigningKey(key) // Définit la clé pour vérifier la signature
//.requireIssuer("MonApplication") // Optionnel: exiger un émetteur spécifique
//.requireAudience("MonAPI") // Optionnel: exiger une audience spécifique
.build()
.parseClaimsJws(token); // Tente de parser et valider. Lève une exception si invalide.
return true; // Si aucune exception n'est levée, le token est valide
} catch (ExpiredJwtException e) {
logger.error("Validation JWT: Token expiré: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("Validation JWT: Token non supporté: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Validation JWT: Token malformé: {}", e.getMessage());
} catch (SignatureException e) {
logger.error("Validation JWT: Signature invalide: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("Validation JWT: Argument invalide ou chaîne vide: {}", e.getMessage());
}
return false; // Le token est invalide
}
Extraction des informations (Claims) après validation
Une fois qu'un token a été validé avec succès par `parseClaimsJws()`, l'étape suivante consiste à extraire les informations utiles (les claims) qu'il contient. Ces informations sont nécessaires pour identifier l'utilisateur et établir son contexte de sécurité dans Spring Security.
La méthode `parseClaimsJws(token)` retourne un objet `Jws
L'objet `Claims` fonctionne comme une Map et permet d'accéder aux différents claims :
- Claims standards : via des méthodes dédiées comme `getSubject()`, `getExpiration()`, `getIssuedAt()`, `getIssuer()`, `getAudience()`, `getId()`.
- Claims personnalisés : via la méthode générique `get(claimName, Type.class)` ou simplement `get(claimName)` et en effectuant un cast.
Exemple d'extraction des claims :
public Claims getAllClaimsFromToken(String token) {
// Assurez-vous que le token est valide avant d'appeler cette méthode,
// ou incluez la validation ici.
String base64Secret = "...";
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
public String getUsernameFromToken(String token) {
return getAllClaimsFromToken(token).getSubject();
}
public Date getExpirationDateFromToken(String token) {
return getAllClaimsFromToken(token).getExpiration();
}
@SuppressWarnings("unchecked")
public List getRolesFromToken(String token) {
// Attention au cast et à la gestion de l'absence du claim
return getAllClaimsFromToken(token).get("roles", List.class);
}
// Utilisation dans le filtre d'authentification
// if (isTokenValid(jwt)) {
// String username = getUsernameFromToken(jwt);
// List roles = getRolesFromToken(jwt);
// // ... construire l'objet Authentication pour SecurityContextHolder ...
// }
Ces informations extraites, notamment le nom d'utilisateur (`subject`) et les rôles/permissions (claims personnalisés), sont ensuite utilisées pour construire l'objet `Authentication` (par exemple, `UsernamePasswordAuthenticationToken`) qui sera placé dans le `SecurityContextHolder` par le filtre d'authentification JWT.