CodeGym /Cours /Spring Security /oauth2ResourceServer ( ) . jwt ( ) : ce qu’il y a dedans

oauth2ResourceServer ( ) . jwt ( ) : ce qu’il y a dedans

Spring Security
Niveau 25, Leçon 0
Disponible

1. Deuxième approche par-dessus le filtre JWT personnalisé

Quand vous avez assemblé l’authentification JWT pour la première fois via votre propre OncePerRequestFilter, le sentiment est généralement : « youpi, j’ai vaincu le monstre ». Et c’est un sentiment honnête — vous avez réellement construit une authentification stateless de vos propres mains et vous avez vu comment une requête se transforme en « current user » à l’intérieur de SecurityContext. Mais alors arrive la question adulte : si nous avons écrit nous‑mêmes le code d’infrastructure, sommes‑nous prêts à le maintenir aussi bien que Spring Security maintient le sien ?

Un custom filter est un excellent outil pédagogique, car il est totalement transparent : vous voyez littéralement chaque ligne où l’en‑tête est extrait, où la signature est validée, où Authentication est créée. Mais dans un projet réel, un tel code devient une zone à risque. Une erreur dans le traitement d’un « token vide », un log superflu contenant le token entier, un mauvais ordre des filtres, une exception avalée par mégarde — et voilà votre API qui vit soudain dans un étrange monde de 401/403 où « tout semble correct, mais le client pleure quand même ».

Le support intégré de Spring Security — celui que nous activons via oauth2ResourceServer().jwt() — n’est pas un nouveau modèle de sécurité ni une « autre philosophie JWT ». C’est la même approche stateless Bearer token, mais assemblée d’une manière native au framework. Autrement dit, nous ne changeons pas le sens, nous changeons simplement « qui tient le tournevis ».

Pour ne pas mélanger deux mécanismes dans une même application lancée, il est pratique de garder le chemin built‑in dans un profil jwt-rs séparé de la même API. La variante avec custom filter reste un point de comparaison, mais dans la chaîne active, un mécanisme Bearer est suffisant.

2. La séparation principale : délivrance du token et vérification du token

La confusion la plus fréquente chez les débutants commence ici : on voit oauth2… on voit resourceServer, et le cerveau fait : « ah, donc c’est à propos de la connexion, de l’inscription, de “donne‑moi un token” ». En réalité, c’est l’inverse. La délivrance du JWT est notre flow applicatif (notre endpoint POST /api/auth/login). Alors que oauth2ResourceServer().jwt() concerne la vérification du Bearer JWT entrant sur chaque requête protégée.

Pour que cela ne reste pas de belles paroles, il est utile de garder en tête une image très simple : un endpoint crée le token, et tous les autres endpoints protégés reçoivent le token et décident si l’on peut faire confiance à la requête.

Un petit schéma (attention : il concerne le niveau métier, pas les détails internes de la chaîne de filtres) :

flowchart LR
    C[Client] -->|"POST /api/auth/login (identifiant+mot de passe)"| API["Secure Content Platform API"]
    API -->|"200 + JWT"| C
    C -->|"GET /api/me Authorization: Bearer JWT"| API
    API -->|"200 + JSON"| C

Et c’est là que se trouve le sens clé : oauth2ResourceServer().jwt() ne remplace pas POST /api/auth/login. Il fait en sorte que les requêtes après la connexion soient vérifiées correctement, sans votre filtre maison.

Pour que le lien avec le projet soit direct, voici un petit « extrait » du contrôleur de connexion (il existe déjà et ne doit pas disparaître après l’activation du chemin built‑in) :

import org.springframework.web.bind.annotation.*;

@RestController
public class AuthController {

    private final AuthService authService;

    // AuthService — votre service métier : vérification login/mot de passe et délivrance JWT
    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @PostMapping("/api/auth/login")
    public AuthResponse login(@RequestBody LoginRequest request) {
        // Important : cet endpoint délivre le token (le resource server built-in ne le fait pas)
        return authService.login(request);
    }
}

Le sens du fragment est simple : l’endpoint reste vôtre. Spring Security ne « connecte pas par magie » ici — il vous fournit juste AuthenticationManager, les providers et l’architecture générale. Quant à la délivrance du token, c’est une décision de l’application.

3. Mode resource server dans le cadre du cours

L’expression « resource server » sonne comme si nous partions maintenant dans le grand monde d’OAuth2, avec un serveur d’autorisation externe, un écran de consentement, des codes, des redirections et autres aventures. Dans ce cours, nous n’y allons pas. Nous utilisons une partie de la mécanique qui résout un problème très concret : accepter un Bearer JWT, le vérifier et restaurer le current user le temps de la requête.

Si on explique le « mode resource server » de manière humaine, on obtient une analogie presque domestique. Imaginez un club avec un vigile. À l’entrée, la personne a un badge (JWT). Le vigile ne délivre pas le badge — il vérifie seulement qu’il est authentique, pas périmé et pas falsifié. Si tout va bien, le vigile appose un tampon « ok » sur la main (c’est l’apparition de l’authentification dans SecurityContext).

Le mot oauth2 dans le DSL est en quelque sorte le « nom historique de la section », car la validation JWT dans Spring Security vit dans le paquetage resource server. Mais notre réalité dans le projet reste la même : nous avons notre propre API, notre propre endpoint de connexion, notre propre TokenService, et nous voulons que les requêtes vers /api/me, /api/drafts, /api/editor/** et /api/admin/** vérifient normalement le Bearer token.

Il est important de le dire aussi parce que sinon l’étudiant commence à « réécrire l’architecture dans sa tête » trop tôt : « puisqu’on a oauth2ResourceServer, il faut jeter AuthenticationManager de la connexion » ou « donc il faut maintenant obtenir le JWT de quelqu’un d’extérieur ». Non. Nous remplaçons simplement le « contrôle aux frontières » fait maison par un contrôle intégré.

4. Modifications dans SecurityFilterChain

Au niveau du code, tout semble suspectement simple — et c’est un bon signe. Nous n’ajoutons pas un troisième modèle de sécurité et ne l’activons pas à côté du custom filter. Nous prenons la même API, nous la lançons dans le profil jwt-rs et nous changeons uniquement la façon de vérifier le Bearer JWT.

La ligne minimale (dans votre SecurityConfig) ressemble à ceci :

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

// Activation du mode resource server : l'application accepte le Bearer JWT en entrée
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {
    // Ici on peut configurer JwtDecoder / JwtAuthenticationConverter etc.
}));

Si l’on assemble un fragment un peu plus réaliste — avec la configuration stateless que nous connaissons déjà — on obtient ceci :

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;

// L’API JWT fonctionne presque toujours sans session : chaque requête porte elle-même le token
http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    // Activation de la vérification intégrée du Bearer JWT
    .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {
        // La conversion claims -> authorities est généralement configurée ici
    }));

Notez deux choses.

Premier point : stateless reste obligatoire. Le fait que nous ayons activé le support JWT ne signifie pas que l’application revient soudainement aux sessions. Au contraire : le mode resource server est l’histoire typique du « chaque requête porte elle‑même l’authentification ».

Deuxième point : oauth2ResourceServer().jwt() n’est pas une « règle d’accès ». Il ne remplace pas authorizeHttpRequests. Vos règles sur les URL et les méthodes, vos hasRole(...), vos hasAuthority(...), vos authenticated() restent en place. Autrement dit, vous devez toujours dire : « /api/public/** — accessible à tous », « /api/admin/** — seulement ADMIN », « tout le reste — au moins authentifié ».

Par exemple (la logique est la même que nous avions déjà ; l’important est de voir qu’elle ne change pas) :

http.authorizeHttpRequests(auth -> auth
    // Routes publiques : connexion et contenu sans authentification
    .requestMatchers("/api/public/**", "/api/auth/**").permitAll()
    // Routes admin : seulement le rôle ADMIN
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    // Tout le reste : JWT valide requis (authentification)
    .anyRequest().authenticated()
);

Nuance critique : un seul mécanisme JWT

Si dans le checkpoint précédent vous aviez ajouté votre propre filtre, par exemple comme ceci :

// Était dans la branche custom JWT (approximativement)
// Important : ce filtre extrayait manuellement le JWT et définissait Authentication dans SecurityContext
http.addFilterBefore(jwtAuthFilter, /* un filtre standard */);

alors, avec le chemin built‑in activé, ce « poste de contrôle artisanal » doit être retiré de la chaîne active. Si la branche custom JWT reste dans le projet pour comparaison, maintenez‑la hors du profil jwt-rs. Dans une application lancée, un seul mécanisme Bearer/JWT est nécessaire. Sinon, vous obtiendrez presque à coup sûr des bizarreries : ici vous définirez SecurityContext, là vous l’écraserez, ici vous traiterez l’erreur deux fois, et le débogage deviendra une série de 12 saisons.

Le principe est simple : dans un profil actif du projet, un seul mécanisme Bearer/JWT.

5. Dépendances Gradle pour le resource server

Quand vous écrivez .oauth2ResourceServer(...).jwt(...), vous dites en quelque sorte : « je veux utiliser les composants prêts à l’emploi pour la vérification JWT ». Mais ces composants ne sortent pas de nulle part. Dans Spring Security, ils sont empaquetés dans des modules séparés, car toutes les applications n’ont pas besoin du mode resource server.

C’est pourquoi, dans le profil jwt-rs du projet, vous aurez besoin des dépendances pour le resource server et JOSE (c’est‑à‑dire pour la partie JWT/JWS : signature, algorithmes, etc.).

Le fragment minimal de build.gradle.kts pourrait ressembler à ceci :

dependencies {
    // Infrastructure de base Spring Security (filtres, configuration, contrats)
    implementation("org.springframework.boot:spring-boot-starter-security")

    // Mode resource server : réception du Bearer token et validation JWT sur les requêtes entrantes
    implementation("org.springframework.security:spring-security-oauth2-resource-server")

    // JOSE : signatures/algorithmes, travail avec JWS/JWT (ce qui permet de vérifier le token)
    implementation("org.springframework.security:spring-security-oauth2-jose")
}

Et il est important de ne pas confondre : spring-boot-starter-security est la « porte générale » vers Spring Security (filtres, configuration, interfaces de base). Alors que resource server + jose est spécifiquement « nous savons analyser un Bearer token et valider un JWT ».

Si ces dépendances sont absentes, votre appel DSL soit ne compilera pas, soit le comportement sera incomplet. Autrement dit, une « belle ligne » ne suffit pas — nous activons une fonctionnalité réelle, et elle doit être dans le classpath.

6. Ce qui ne change pas dans le projet

Après l’activation du support JWT built‑in, il n’est pas nécessaire de réécrire toute la couche de sécurité. Seule l’infrastructure de vérification du token à l’entrée change.

POST /api/auth/login reste votre flow applicatif et continue de délivrer le token via TokenService. authorizeHttpRequests reste la carte d’accès : les routes publiques, /api/me, les zones editor/admin sont décrites avec les mêmes règles qu’avant.

Il en va de même pour la sécurité au niveau des méthodes et les vérifications de propriétaire (« owner‑based »). Peu importe au service que Authentication provienne d’une session, de HTTP Basic, d’un custom JWT filter ou de oauth2ResourceServer().jwt(). Ce qui compte pour lui, c’est qu’il y ait un utilisateur et un ensemble d’authorities dans SecurityContext à partir desquels sont décidées la publication, la modération et les règles « propriétaire / étranger ».

7. Erreurs typiques

Les erreurs à ce stade ne portent généralement pas sur « je ne connais pas l’annotation », mais sur un mauvais modèle mental. Les gens commencent à attendre du chemin JWT built‑in ce qu’il ne doit pas faire par définition, ou au contraire essaient de jeter des parties du système qui restent obligatoires. Voici les râteaux les plus fréquents sur lesquels presque tout le monde marche (comme des LEGO, mais en plus douloureux).

Erreur n°1 : penser que oauth2ResourceServer().jwt() « fait la connexion ».
Le support JWT intégré ne remplace pas POST /api/auth/login. Il ne sait pas authentifier un utilisateur par nom d’utilisateur/mot de passe, ne délivre pas de token et ne connaît pas votre logique métier. Son domaine de responsabilité, ce sont les requêtes qui arrivent déjà avec un Bearer token et leur vérification à l’entrée.

Erreur n°2 : oublier que le mode stateless reste une décision obligatoire.
Parfois, on active le chemin JWT built‑in mais on laisse la configuration comme si l’application vivait dans un monde basé sur les sessions. Résultat : des effets de bord étranges apparaissent — ici on attend une session, là non, et SecurityContext se comporte « pas comme dans les leçons ». Si vous construisez une API uniquement JWT, gardez SessionCreationPolicy.STATELESS comme un choix explicite et conscient.

Erreur n°3 : s’attendre à ce que les règles d’accès « se remplacent toutes seules ».
oauth2ResourceServer().jwt() n’est pas authorizeHttpRequests. Il ne décide pas qui peut accéder à /api/admin/**. Il ne fait que créer l’authentification (si le token est valide). Ensuite, ce sont vos règles (hasRole(...), hasAuthority(...), @PreAuthorize) qui décident de ce que cet utilisateur a le droit de faire.

Erreur n°4 : laisser à la fois le custom JWT filter et le chemin built‑in dans la chaîne.
C’est l’une des erreurs les plus désagréables, car elle ne « tombe pas toujours immédiatement ». Parfois tout démarre, mais le comportement devient imprévisible : doubles tentatives d’authentification, formats d’erreur différents, des 403 inattendus là où vous attendiez un 401. Pour un ensemble d’endpoints donné, choisissez une seule approche. Si les deux variantes coexistent dans le même dépôt, séparez‑les dans des profils différents et n’en lancez qu’un à la fois.

Erreur n°5 : se mettre à « réparer » l’autorisation métier alors que le problème ne vient pas du tout de là.
Si après l’activation du chemin JWT built‑in vous constatez un refus inattendu, le premier réflexe est de réécrire @PreAuthorize ou de remplacer hasAuthority par hasRole. Souvent c’est prématuré : avant de casser les règles métier, assurez‑vous de bien faire la distinction entre « l’utilisateur n’est pas authentifié » et « l’utilisateur est authentifié mais n’a pas les droits ». C’est précisément cette distinction qui sauve le projet du chaos dans les 401/403 et aide à ne pas traiter le symptôme au lieu de la cause.

1
Mission
Spring Security, niveau 25, leçon 0
Bloqué
Protection Bearer/JWT intégrée pour une API simple
Protection Bearer/JWT intégrée pour une API simple
1
Mission
Spring Security, niveau 25, leçon 0
Bloqué
Flux de connexion séparé avec le chemin JWT intégré
Flux de connexion séparé avec le chemin JWT intégré
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION