Spring Security 6 : Authentification JWT complète

Guide pratique pour implémenter une authentification JWT avec Spring Security 6. Configuration, génération de tokens, validation et bonnes pratiques.

Authentification JWT avec Spring Security 6

L'authentification JWT (JSON Web Token) représente la norme pour sécuriser les API REST modernes. Spring Security 6 introduit une nouvelle approche fonctionnelle de configuration qui simplifie l'implémentation tout en renforçant la sécurité. Ce guide couvre l'ensemble du processus, de la configuration initiale à la protection des endpoints.

Prérequis

Ce tutoriel utilise Spring Boot 3.2+ et Spring Security 6.2+. Les concepts restent valables pour les versions ultérieures, avec des ajustements mineurs de syntaxe.

Architecture JWT dans Spring Security

L'authentification JWT repose sur un principe stateless : le serveur ne stocke aucune session. Chaque requête contient un token signé qui prouve l'identité de l'utilisateur. Cette architecture permet une scalabilité horizontale sans partage de session entre instances.

Le flux d'authentification JWT se décompose en plusieurs étapes. L'utilisateur s'authentifie avec ses identifiants, reçoit un token JWT signé, puis inclut ce token dans chaque requête ultérieure. Le serveur valide la signature et extrait les informations utilisateur du token.

JwtAuthenticationFlow.javajava
// Représentation conceptuelle du flux d'authentification
public class JwtAuthenticationFlow {

    // 1. Authentification initiale : POST /api/auth/login
    // → Vérification identifiants en base
    // → Génération du token JWT signé
    // → Retour du token au client

    // 2. Requêtes authentifiées : GET /api/protected
    // → Header: Authorization: Bearer <token>
    // → Extraction et validation du token
    // → Création du SecurityContext
    // → Accès à la ressource protégée
}

Cette approche élimine les problèmes de sticky sessions et simplifie le déploiement en environnement distribué.

Configuration des dépendances Maven

Le projet nécessite les dépendances Spring Security et une bibliothèque JWT. JJWT (Java JWT) offre une API fluide et bien maintenue pour la manipulation des tokens.

xml
<!-- pom.xml -->
<dependencies>
    <!-- Spring Security pour la gestion de l'authentification -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Spring Web pour les endpoints REST -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- JJWT : génération et validation des tokens -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.5</version>
        <scope>runtime</scope>
    </dependency>

    <!-- Spring Data JPA pour la persistance utilisateur -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

La séparation en trois modules JJWT (api, impl, jackson) suit le principe d'encapsulation : seule l'API est visible au compile-time, l'implémentation reste un détail runtime.

Service de génération et validation JWT

Le service JWT centralise toutes les opérations sur les tokens : génération, extraction des claims et validation. Une clé secrète sécurisée signe chaque token, garantissant son intégrité.

JwtService.javajava
@Service
public class JwtService {

    // Clé secrète injectée depuis application.yml
    @Value("${app.jwt.secret}")
    private String secretKey;

    // Durée de validité du token (24 heures par défaut)
    @Value("${app.jwt.expiration:86400000}")
    private long jwtExpiration;

    // Génère un token JWT pour un utilisateur authentifié
    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    // Génère un token avec des claims personnalisés
    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return Jwts.builder()
            .claims(extraClaims)                           // Claims additionnels (rôles, permissions)
            .subject(userDetails.getUsername())            // Identifiant principal
            .issuedAt(new Date())                          // Date de création
            .expiration(new Date(System.currentTimeMillis() + jwtExpiration))
            .signWith(getSigningKey(), Jwts.SIG.HS256)     // Signature HMAC-SHA256
            .compact();
    }

    // Extrait le username (subject) du token
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    // Extrait un claim spécifique via une fonction d'extraction
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    // Valide le token : signature correcte et non expiré
    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    // Vérifie si le token a expiré
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    // Extrait la date d'expiration
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    // Parse le token et extrait tous les claims
    private Claims extractAllClaims(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())    // Vérifie la signature
            .build()
            .parseSignedClaims(token)       // Parse le token signé
            .getPayload();                  // Retourne les claims
    }

    // Génère la clé de signature depuis la clé secrète encodée en Base64
    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

La clé secrète doit être suffisamment longue (256 bits minimum pour HS256) et stockée de manière sécurisée, jamais dans le code source.

Sécurité de la clé secrète

Utilisez une variable d'environnement ou un gestionnaire de secrets pour la clé JWT. Une clé compromise permet de forger des tokens valides pour n'importe quel utilisateur.

Configuration de l'entité User

L'entité utilisateur implémente UserDetails de Spring Security, permettant une intégration directe avec le système d'authentification.

User.javajava
@Entity
@Table(name = "users")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String firstName;

    @Column(nullable = false)
    private String lastName;

    // Rôle stocké comme enum pour la sécurité de type
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    // Implémentation UserDetails : retourne les autorités de l'utilisateur
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }

    // Le username correspond à l'email dans cette implémentation
    @Override
    public String getUsername() {
        return email;
    }

    // Compte toujours actif (à adapter selon les besoins)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // Getters et setters omis pour la concision
}
Role.javajava
public enum Role {
    USER,      // Utilisateur standard
    ADMIN      // Administrateur avec privilèges étendus
}

L'enum Role limite les valeurs possibles et facilite la vérification des autorisations dans les expressions SpEL.

Filtre d'authentification JWT

Le filtre JWT intercepte chaque requête pour extraire et valider le token. Il s'insère dans la chaîne de filtres Spring Security avant le filtre d'authentification standard.

JwtAuthenticationFilter.javajava
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {

        // Récupère le header Authorization
        final String authHeader = request.getHeader("Authorization");

        // Vérifie la présence du préfixe Bearer
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        // Extrait le token (sans le préfixe "Bearer ")
        final String jwt = authHeader.substring(7);

        try {
            // Extrait le username du token
            final String userEmail = jwtService.extractUsername(jwt);

            // Vérifie que l'utilisateur n'est pas déjà authentifié
            if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                // Charge les détails utilisateur depuis la base
                UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);

                // Valide le token (signature + expiration + correspondance user)
                if (jwtService.isTokenValid(jwt, userDetails)) {

                    // Crée l'objet d'authentification
                    UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                        );

                    // Ajoute les détails de la requête
                    authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                    );

                    // Définit l'authentification dans le contexte de sécurité
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
        } catch (ExpiredJwtException e) {
            // Token expiré : l'utilisateur devra se réauthentifier
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Token expired");
            return;
        } catch (JwtException e) {
            // Token invalide : signature incorrecte ou format malformé
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Invalid token");
            return;
        }

        // Continue la chaîne de filtres
        filterChain.doFilter(request, response);
    }
}

OncePerRequestFilter garantit que le filtre s'exécute une seule fois par requête, même en cas de forward ou include.

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Configuration Spring Security 6

Spring Security 6 utilise une approche fonctionnelle avec des lambdas pour configurer la chaîne de sécurité. Cette configuration définit les règles d'accès et intègre le filtre JWT.

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // Active @PreAuthorize et @PostAuthorize
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            // Désactive CSRF car l'API est stateless (pas de cookies de session)
            .csrf(csrf -> csrf.disable())

            // Configure les règles d'autorisation
            .authorizeHttpRequests(auth -> auth
                // Endpoints publics : authentification et inscription
                .requestMatchers("/api/auth/**").permitAll()
                // Documentation API accessible sans authentification
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                // Endpoints admin réservés aux administrateurs
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                // Toutes les autres requêtes nécessitent une authentification
                .anyRequest().authenticated()
            )

            // Mode stateless : pas de session HTTP côté serveur
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // Fournisseur d'authentification personnalisé
            .authenticationProvider(authenticationProvider)

            // Insère le filtre JWT avant le filtre d'authentification standard
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

            .build();
    }
}
ApplicationConfig.javajava
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {

    private final UserRepository userRepository;

    // Service de chargement des utilisateurs pour Spring Security
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
    }

    // Fournisseur d'authentification avec encoder de mot de passe
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    // Gestionnaire d'authentification exposé comme bean
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
        throws Exception {
        return config.getAuthenticationManager();
    }

    // Encoder BCrypt pour le hashage sécurisé des mots de passe
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

La désactivation de CSRF est sécurisée pour les API stateless car l'authentification repose sur un header explicite, non sur un cookie automatiquement envoyé par le navigateur.

Contrôleur d'authentification

Le contrôleur expose les endpoints d'inscription et de connexion. Ces endpoints sont publics et retournent le token JWT après une authentification réussie.

AuthenticationController.javajava
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService authService;

    // POST /api/auth/register - Inscription d'un nouvel utilisateur
    @PostMapping("/register")
    public ResponseEntity<AuthenticationResponse> register(
        @Valid @RequestBody RegisterRequest request
    ) {
        return ResponseEntity.ok(authService.register(request));
    }

    // POST /api/auth/login - Connexion utilisateur existant
    @PostMapping("/login")
    public ResponseEntity<AuthenticationResponse> login(
        @Valid @RequestBody AuthenticationRequest request
    ) {
        return ResponseEntity.ok(authService.authenticate(request));
    }

    // POST /api/auth/refresh - Rafraîchissement du token (optionnel)
    @PostMapping("/refresh")
    public ResponseEntity<AuthenticationResponse> refresh(
        @RequestHeader("Authorization") String authHeader
    ) {
        return ResponseEntity.ok(authService.refreshToken(authHeader));
    }
}
AuthenticationRequest.javajava
public record AuthenticationRequest(
    @NotBlank(message = "Email requis")
    @Email(message = "Format email invalide")
    String email,

    @NotBlank(message = "Mot de passe requis")
    String password
) {}
RegisterRequest.javajava
public record RegisterRequest(
    @NotBlank(message = "Prénom requis")
    String firstName,

    @NotBlank(message = "Nom requis")
    String lastName,

    @NotBlank(message = "Email requis")
    @Email(message = "Format email invalide")
    String email,

    @NotBlank(message = "Mot de passe requis")
    @Size(min = 8, message = "Le mot de passe doit contenir au moins 8 caractères")
    String password
) {}
AuthenticationResponse.javajava
public record AuthenticationResponse(
    String token,
    String type,
    long expiresIn
) {
    public AuthenticationResponse(String token, long expiresIn) {
        this(token, "Bearer", expiresIn);
    }
}

Les records Java simplifient la définition des DTOs tout en garantissant l'immuabilité.

Service d'authentification

Le service orchestre la logique d'inscription et d'authentification, en déléguant au JwtService pour la génération des tokens.

AuthenticationService.javajava
@Service
@RequiredArgsConstructor
public class AuthenticationService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;
    private final AuthenticationManager authenticationManager;

    @Value("${app.jwt.expiration:86400000}")
    private long jwtExpiration;

    // Inscription d'un nouvel utilisateur
    @Transactional
    public AuthenticationResponse register(RegisterRequest request) {
        // Vérifie que l'email n'est pas déjà utilisé
        if (userRepository.existsByEmail(request.email())) {
            throw new EmailAlreadyExistsException("Email déjà enregistré");
        }

        // Crée l'entité utilisateur avec mot de passe hashé
        User user = new User();
        user.setFirstName(request.firstName());
        user.setLastName(request.lastName());
        user.setEmail(request.email());
        user.setPassword(passwordEncoder.encode(request.password()));
        user.setRole(Role.USER);

        // Persiste l'utilisateur
        userRepository.save(user);

        // Génère et retourne le token JWT
        String jwtToken = jwtService.generateToken(user);
        return new AuthenticationResponse(jwtToken, jwtExpiration);
    }

    // Authentification d'un utilisateur existant
    public AuthenticationResponse authenticate(AuthenticationRequest request) {
        // Délègue la vérification au AuthenticationManager
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.email(),
                request.password()
            )
        );

        // Charge l'utilisateur (l'authentification a réussi)
        User user = userRepository.findByEmail(request.email())
            .orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé"));

        // Génère le token avec claims additionnels
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", user.getRole().name());
        claims.put("userId", user.getId());

        String jwtToken = jwtService.generateToken(claims, user);
        return new AuthenticationResponse(jwtToken, jwtExpiration);
    }

    // Rafraîchissement du token (prolonge la session)
    public AuthenticationResponse refreshToken(String authHeader) {
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new InvalidTokenException("Token invalide");
        }

        String oldToken = authHeader.substring(7);
        String userEmail = jwtService.extractUsername(oldToken);

        User user = userRepository.findByEmail(userEmail)
            .orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé"));

        // Génère un nouveau token
        String newToken = jwtService.generateToken(user);
        return new AuthenticationResponse(newToken, jwtExpiration);
    }
}

L'AuthenticationManager centralise la vérification des identifiants, permettant de changer facilement de stratégie d'authentification.

Refresh Token

Pour une sécurité renforcée, implémentez un système de refresh token avec une durée de vie plus longue, stocké en base de données et révocable.

Protection des endpoints avec annotations

La sécurité au niveau des méthodes permet un contrôle granulaire des autorisations directement dans le code métier.

UserController.javajava
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // Accessible à tous les utilisateurs authentifiés
    @GetMapping("/me")
    public ResponseEntity<UserDTO> getCurrentUser(
        @AuthenticationPrincipal User currentUser
    ) {
        return ResponseEntity.ok(UserDTO.from(currentUser));
    }

    // Seul l'utilisateur concerné ou un admin peut modifier le profil
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    @PutMapping("/{id}")
    public ResponseEntity<UserDTO> updateUser(
        @PathVariable Long id,
        @Valid @RequestBody UpdateUserRequest request
    ) {
        return ResponseEntity.ok(userService.updateUser(id, request));
    }

    // Réservé aux administrateurs
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping
    public ResponseEntity<List<UserDTO>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }

    // Suppression avec vérification post-exécution
    @PreAuthorize("hasRole('ADMIN')")
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}
AdminController.javajava
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")  // Toutes les méthodes requièrent ADMIN
public class AdminController {

    private final AdminService adminService;

    @GetMapping("/dashboard")
    public ResponseEntity<DashboardDTO> getDashboard() {
        return ResponseEntity.ok(adminService.getDashboardStats());
    }

    @PostMapping("/users/{id}/role")
    public ResponseEntity<UserDTO> changeUserRole(
        @PathVariable Long id,
        @RequestParam Role newRole
    ) {
        return ResponseEntity.ok(adminService.changeUserRole(id, newRole));
    }
}

L'annotation @AuthenticationPrincipal injecte directement l'utilisateur authentifié, évitant l'accès manuel au SecurityContext.

Gestion des erreurs d'authentification

Une gestion centralisée des erreurs assure des réponses cohérentes et informatives en cas d'échec d'authentification.

SecurityExceptionHandler.javajava
@RestControllerAdvice
public class SecurityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(SecurityExceptionHandler.class);

    // Erreur d'authentification (identifiants incorrects)
    @ExceptionHandler(BadCredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleBadCredentials(BadCredentialsException ex) {
        return new ErrorResponse(
            "INVALID_CREDENTIALS",
            "Email ou mot de passe incorrect",
            null
        );
    }

    // Accès refusé (authentifié mais non autorisé)
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ErrorResponse handleAccessDenied(AccessDeniedException ex) {
        return new ErrorResponse(
            "ACCESS_DENIED",
            "Accès non autorisé à cette ressource",
            null
        );
    }

    // Token JWT expiré
    @ExceptionHandler(ExpiredJwtException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleExpiredToken(ExpiredJwtException ex) {
        return new ErrorResponse(
            "TOKEN_EXPIRED",
            "Session expirée, veuillez vous reconnecter",
            null
        );
    }

    // Token JWT invalide
    @ExceptionHandler(JwtException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ErrorResponse handleInvalidToken(JwtException ex) {
        log.warn("Invalid JWT token: {}", ex.getMessage());
        return new ErrorResponse(
            "INVALID_TOKEN",
            "Token d'authentification invalide",
            null
        );
    }

    // Email déjà utilisé lors de l'inscription
    @ExceptionHandler(EmailAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ErrorResponse handleEmailExists(EmailAlreadyExistsException ex) {
        return new ErrorResponse(
            "EMAIL_EXISTS",
            ex.getMessage(),
            null
        );
    }
}
ErrorResponse.javajava
public record ErrorResponse(
    String code,
    String message,
    Map<String, String> details
) {}

Les codes d'erreur standardisés facilitent le traitement côté client et le debugging.

Configuration application.yml

La configuration externalisée permet d'adapter les paramètres JWT selon l'environnement.

yaml
# application.yml
app:
  jwt:
    # Clé secrète en Base64 (256 bits minimum pour HS256)
    # Générer avec : openssl rand -base64 32
    secret: ${JWT_SECRET:votreCleSuperSecreteDe256BitsMinimum}
    # Durée de validité en millisecondes (24 heures)
    expiration: 86400000

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/springjwt
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:postgres}
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
yaml
# application-dev.yml
app:
  jwt:
    # Expiration courte pour le développement (1 heure)
    expiration: 3600000

spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

logging:
  level:
    org.springframework.security: DEBUG

En production, la clé secrète doit provenir d'une variable d'environnement ou d'un service de gestion des secrets comme Vault.

Tests d'intégration

Les tests vérifient le comportement complet du système d'authentification, de l'inscription à l'accès aux ressources protégées.

AuthenticationIntegrationTest.javajava
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = Replace.ANY)
class AuthenticationIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }

    @Test
    void shouldRegisterAndAuthenticateUser() {
        // Inscription
        RegisterRequest registerRequest = new RegisterRequest(
            "John", "Doe", "john@example.com", "password123"
        );

        ResponseEntity<AuthenticationResponse> registerResponse = restTemplate
            .postForEntity("/api/auth/register", registerRequest, AuthenticationResponse.class);

        assertThat(registerResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(registerResponse.getBody().token()).isNotBlank();

        // Connexion avec les mêmes identifiants
        AuthenticationRequest loginRequest = new AuthenticationRequest(
            "john@example.com", "password123"
        );

        ResponseEntity<AuthenticationResponse> loginResponse = restTemplate
            .postForEntity("/api/auth/login", loginRequest, AuthenticationResponse.class);

        assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(loginResponse.getBody().token()).isNotBlank();
    }

    @Test
    void shouldRejectInvalidCredentials() {
        AuthenticationRequest request = new AuthenticationRequest(
            "unknown@example.com", "wrongpassword"
        );

        ResponseEntity<ErrorResponse> response = restTemplate
            .postForEntity("/api/auth/login", request, ErrorResponse.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }

    @Test
    void shouldAccessProtectedResourceWithValidToken() {
        // Inscription pour obtenir un token
        RegisterRequest registerRequest = new RegisterRequest(
            "Jane", "Doe", "jane@example.com", "password123"
        );

        AuthenticationResponse authResponse = restTemplate
            .postForObject("/api/auth/register", registerRequest, AuthenticationResponse.class);

        // Accès à la ressource protégée avec le token
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(authResponse.token());

        ResponseEntity<UserDTO> response = restTemplate.exchange(
            "/api/users/me",
            HttpMethod.GET,
            new HttpEntity<>(headers),
            UserDTO.class
        );

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().email()).isEqualTo("jane@example.com");
    }

    @Test
    void shouldRejectAccessWithoutToken() {
        ResponseEntity<Void> response = restTemplate
            .getForEntity("/api/users/me", Void.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }
}

Ces tests couvrent les scénarios principaux : inscription, connexion, accès autorisé et accès refusé.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Conclusion

L'implémentation d'une authentification JWT avec Spring Security 6 suit un pattern bien défini. Le service JWT gère la génération et validation des tokens, le filtre intercepte les requêtes pour établir le contexte de sécurité, et la configuration définit les règles d'accès.

Checklist de déploiement :

  • ✅ Clé secrète de 256 bits minimum stockée en variable d'environnement
  • ✅ HTTPS obligatoire en production pour protéger les tokens en transit
  • ✅ Durée de validité des tokens adaptée au contexte (15 min à 24h)
  • ✅ Refresh token pour les sessions longues sans réauthentification
  • ✅ Gestion des erreurs standardisée avec codes métier
  • ✅ Logging des tentatives d'authentification échouées
  • ✅ Tests d'intégration couvrant les scénarios d'authentification
  • ✅ Rate limiting sur les endpoints d'authentification

Cette architecture offre une base solide pour sécuriser les API REST, tout en restant extensible pour des besoins plus complexes comme l'authentification multi-facteurs ou l'intégration OAuth2.

Tags

#spring security
#jwt
#java
#spring boot
#authentification

Partager

Articles similaires