Mettre en place une authentification par jetons dans une API Spring ne consiste pas seulement à brancher une dépendance et à ouvrir quelques endpoints. Il faut comprendre ce que Spring Security valide réellement, ce que le client transporte, et où se trouvent les vrais points de fragilité en production. Je vais aller droit au but: comment fonctionne le flux JWT, comment le configurer proprement, quels claims vérifier, comment convertir ces informations en droits, et quels arbitrages faire entre JWT, jeton opaque et session.
Les points à verrouiller avant de confier des jetons à Spring Security
- Spring Security agit ici comme resource server et valide un Bearer token, pas comme serveur d’identité.
- La signature ne suffit pas: iss, aud, exp et nbf doivent être contrôlés explicitement.
- Les scopes et claims métiers doivent être transformés en autorités exploitables par Spring Security.
- Un access token court est généralement plus sain, souvent 5 à 15 minutes selon le niveau de risque.
- JWT est pratique pour les APIs distribuées, mais la révocation immédiate reste plus simple avec un jeton opaque ou une session.

Ce que Spring Security fait réellement avec un jeton JWT
Le point le plus important à garder en tête est simple: Spring Security ne “fait pas confiance” au jeton, il le vérifie. Dans une API protégée, le client envoie en général un en-tête Authorization: Bearer ..., puis la chaîne de filtres extrait ce jeton, le valide et construit un contexte d’authentification pour la requête en cours.
- Le client appelle l’API avec un jeton Bearer.
- Le filtre dédié extrait le jeton et le remet à la chaîne d’authentification.
- Le validateur vérifie la signature avec la clé publique attendue, puis contrôle les claims critiques.
- Les claims utiles sont transformés en autorités ou en identités applicatives.
- Spring Security place le résultat dans le
SecurityContextet laisse les règles d’autorisation décider.
Je vois souvent une confusion à ce stade: un JWT signé n’est pas un coffre-fort. Il garantit l’intégrité et l’authenticité du message, pas sa confidentialité. Si le token est un JWS classique, son contenu reste lisible; il faut donc éviter d’y mettre des données sensibles que vous ne voudriez pas exposer à un client légitime mais curieux. Une fois ce mécanisme clarifié, la vraie question devient la configuration minimale qui tient la route.
Configurer un resource server proprement
Dans la pratique, la configuration la plus robuste commence par un principe: l’API consomme un serveur d’autorisation, elle ne réinvente pas l’authentification. Avec Spring Boot, cela se traduit généralement par l’ajout du starter resource server, puis par la déclaration de l’émetteur du jeton et, si nécessaire, de l’audience attendue.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.exemple.fr/realms/produit
audiences: api-backofficeCette configuration est volontairement sobre. Quand issuer-uri est connu, Spring peut découvrir les métadonnées nécessaires, récupérer le jeu de clés publiques et vérifier que le jeton vient bien de l’émetteur attendu. En d’autres termes, vous évitez de coder en dur une logique de validation fragile, et vous laissez le framework faire ce pour quoi il est conçu.
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin.read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
return http.build();
}Le détail qui change tout, c’est que je n’utilise pas cette configuration pour “fabriquer” l’authentification, mais pour la restreindre correctement. Une API trop permissive fonctionne en test; en production, elle devient vite un point de passage trop ouvert. Avec cette base, il faut maintenant regarder ce qu’il faut vérifier dans le jeton lui-même.
Valider les claims qui comptent vraiment
Beaucoup d’équipes s’arrêtent à la signature, puis découvrent trop tard qu’un jeton valide peut quand même être inadapté au contexte. Je vérifie systématiquement les claims qui définissent le cadre d’usage du jeton, pas seulement son intégrité cryptographique.
| Claim | Rôle | Ce que je contrôle |
|---|---|---|
iss |
Émetteur du jeton | Je n’accepte qu’un émetteur de confiance, explicitement prévu. |
aud |
Audience visée | Je refuse un jeton émis pour un autre service. |
exp |
Expiration | Je privilégie des durées courtes et j’exige un rejet net après expiration. |
nbf |
Pas encore valide avant | Je limite la tolérance d’horloge à quelques dizaines de secondes au maximum. |
sub |
Sujet principal | Je l’utilise comme identifiant stable, pas comme libellé décoratif. |
Je fixe aussi les algorithmes acceptés. Ce point est souvent sous-estimé: laisser une application “accepter ce qu’elle sait lire” est une mauvaise idée. Je préfère une politique nette, des clés JWK maîtrisées, et une rotation testée avant d’en avoir besoin. Si vous avez plusieurs émetteurs ou plusieurs tenants, Spring Security sait aussi gérer ce cas, mais il faut alors être rigoureux sur la résolution du bon émetteur et sur la propagation du tenant dans le reste du traitement.
Sur les durées, mon repère pratique est simple: un access token court, souvent entre 5 et 15 minutes, puis un mécanisme de renouvellement séparé si le besoin métier le justifie. Plus le jeton vit longtemps, plus le compromis sécurité/expérience utilisateur devient sensible. Une fois ce cadre posé, il reste à transformer les claims en droits réellement exploitables par l’application.
Transformer les claims en autorisations utiles
L’étape suivante n’est pas cosmétique: c’est ce qui relie le jeton aux règles métier. Spring Security sait convertir des claims en authorities, et c’est là que les scopes, rôles et permissions deviennent réellement utilisables dans les règles d’accès.
Par défaut, les scopes sont souvent exposés avec un préfixe SCOPE_. C’est pratique pour des API simples, par exemple SCOPE_orders.read ou SCOPE_invoices.write. Dès que le schéma d’autorisations devient plus métier, je préfère un mapping explicite, surtout si le fournisseur d’identité utilise un claim personnalisé.
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authorities = new JwtGrantedAuthoritiesConverter();
authorities.setAuthoritiesClaimName("authorities");
authorities.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authorities);
converter.setPrincipalClaimName("preferred_username");
return converter;
}Ce code est intéressant pour une raison précise: il sépare l’identité technique du jeton et les autorisations opérationnelles. Je peux donc authentifier un utilisateur avec sub ou preferred_username, tout en gardant des règles d’autorisation lisibles dans le code, par exemple avec @PreAuthorize ou des règles HTTP ciblées. Cette séparation évite une erreur fréquente: confondre un identifiant de sujet avec un rôle.
Quand je relis une base Spring Security mal pensée, le problème n’est presque jamais l’absence de JWT; c’est un mapping trop flou entre ce que contient le jeton et ce que l’application croit pouvoir faire avec. C’est justement là que les incidents commencent.
Les erreurs qui reviennent le plus souvent en production
Je retrouve toujours les mêmes défauts, quel que soit le projet ou la taille de l’équipe. Le premier est de croire qu’un JWT signé est automatiquement sûr parce qu’il est “cryptographique”. En réalité, il peut être parfaitement signé et pourtant mal ciblé, trop long à vivre, ou porteur d’informations mal choisies.
| Erreur | Pourquoi c’est risqué | Ce que je fais à la place |
|---|---|---|
Ne pas vérifier aud
|
Un jeton émis pour un autre service peut être accepté à tort. | Je fixe une audience précise et je la rejette si elle ne correspond pas. |
| Allonger trop la durée de vie | Une compromission reste exploitable trop longtemps. | Je garde des access tokens courts et je sépare le renouvellement. |
| Mettre des données sensibles dans le payload | Le contenu d’un JWT signé reste lisible. | Je limite le payload au strict nécessaire. |
| Loguer le token complet | Les journaux deviennent une fuite de secrets. | Je masque le jeton dans les logs et les traces. |
| Accepter plusieurs comportements d’algorithme sans règle claire | La validation devient floue et plus difficile à auditer. | Je fixe les algorithmes attendus et je teste les refus. |
Il y a aussi un piège de design côté navigateur. Si vous stockez le jeton dans un cookie, vous devez penser CSRF. Si vous le gardez dans le stockage web côté client, vous devez penser XSS. Il n’existe pas de choix magique; il existe seulement un compromis cohérent avec votre surface d’attaque. C’est aussi pour cela qu’il faut choisir la bonne forme de jeton plutôt que d’imposer JWT partout par réflexe.
Choisir entre JWT, jeton opaque et session selon le contexte
La bonne question n’est pas “JWT ou pas JWT”, mais quel niveau de contrôle du cycle de vie du jeton l’équipe veut réellement assumer. Quand je compare les options, je regarde surtout la révocation, l’indépendance réseau et la simplicité d’exploitation.
| Approche | Quand je la choisis | Point fort | Limite principale |
|---|---|---|---|
| JWT | APIs distribuées, plusieurs services, besoin de validation locale | Stateless, rapide à vérifier, adapté aux architectures découpées | Révocation moins immédiate, vigilance accrue sur les claims et la durée de vie |
| Jeton opaque | Quand la révocation immédiate est prioritaire | Contrôle centralisé via introspection | Dépendance à l’autorisation server pour chaque vérification |
| Session | Applications web classiques, back-office, monolithe ou front serveur | Simplicité d’exploitation et de révocation | Moins naturel pour les APIs distribuées et les clients hétérogènes |
Mon arbitrage est assez stable: JWT pour une API moderne consommée par plusieurs clients ou services, opaque token quand la révocation doit rester centrale et immédiate, session quand la simplicité prime sur la distribution. Pour un projet d’entreprise, je regarde aussi le coût opérationnel: surveillance des clés, stratégie de rotation, redémarrages, découpage des responsabilités entre équipe API et équipe identité. C’est souvent là que se gagne ou se perd la robustesse réelle du système.
Ce que je verrouillerais avant de passer l’API en production
Si je devais résumer la mise en production en quelques vérifications concrètes, je commencerais par celles-ci:
- valider l’émetteur avec
issuer-uriet l’audience avecaudiences; - réduire la durée de vie du jeton d’accès au strict nécessaire;
- tester un token expiré, un token mal signé et un token avec mauvaise audience;
- documenter le mapping entre claims et autorités dans l’équipe;
- masquer les jetons dans les logs et les traces applicatives;
- vérifier la rotation des clés avant un changement d’environnement ou de fournisseur d’identité;
- garder une stratégie claire pour les cas multi-tenant si plusieurs émetteurs existent.
En pratique, je cherche toujours la même combinaison: un émetteur unique ou maîtrisé, une audience explicite, des jetons courts, un mapping de claims lisible et un refus net de tout ce qui sort du cadre. C’est cette discipline qui rend l’authentification par jetons avec Spring Security fiable, maintenable et compréhensible pour une équipe produit comme pour une équipe sécurité.