Contactez-nous

Tests de sécurité avec Spring Security Test

Apprenez à écrire des tests efficaces pour vos configurations Spring Security et vos endpoints protégés en utilisant Spring Security Test, MockMvc et les annotations dédiées.

Pourquoi et comment tester la sécurité de vos applications ?

La configuration de la sécurité dans une application Spring Boot, bien que facilitée par le framework, peut devenir complexe. Il est essentiel de s'assurer que les règles d'accès (qui peut accéder à quoi ?) et les mécanismes d'authentification fonctionnent comme prévu. Des erreurs de configuration peuvent entraîner des failles de sécurité critiques, exposant des données sensibles ou permettant des actions non autorisées.

Tester la sécurité ne consiste pas seulement à vérifier qu'un utilisateur peut se connecter, mais aussi à valider que :

  • Les endpoints publics sont accessibles sans authentification.
  • Les endpoints protégés nécessitent une authentification valide.
  • Les endpoints restreints par rôle/autorité ne sont accessibles qu'aux utilisateurs disposant des droits appropriés.
  • Les utilisateurs non authentifiés sont correctement redirigés ou reçoivent une erreur 401/403.
  • La sécurité au niveau des méthodes (`@PreAuthorize`, `@PostAuthorize`, etc.) fonctionne correctement.

Heureusement, l'écosystème Spring fournit le module Spring Security Test (`spring-security-test`), qui s'intègre parfaitement avec les outils de test Spring Boot standard (comme `MockMvc` et JUnit 5) pour simplifier considérablement l'écriture de ces tests.

Mise en place et dépendances

La bonne nouvelle est que si vous utilisez `spring-boot-starter-security` et `spring-boot-starter-test` dans votre projet Maven ou Gradle, la dépendance `spring-security-test` est généralement incluse transitivement. Vous n'avez souvent rien de plus à ajouter.

Vérifiez votre fichier `pom.xml` ou `build.gradle`. Si, pour une raison quelconque, elle n'est pas présente, vous pouvez l'ajouter explicitement :



    org.springframework.security
    spring-security-test
    test

// Gradle
testImplementation 'org.springframework.security:spring-security-test'

Vos classes de test de sécurité utiliseront généralement les annotations Spring Boot Test standard, notamment `@SpringBootTest` pour charger le contexte de l'application et `@AutoConfigureMockMvc` pour injecter un bean `MockMvc`, qui est l'outil principal pour tester la couche web.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTests {

    @Autowired
    private MockMvc mockMvc;

    // Vos tests viendront ici...
}

Tester la sécurité de la couche Web (HttpSecurity)

L'objectif principal est de vérifier que les règles définies dans votre configuration `SecurityFilterChain` (avec `http.authorizeHttpRequests(...)`) sont correctement appliquées.

Tests avec un utilisateur simulé (`@WithMockUser`) :

L'annotation `@WithMockUser` est l'outil le plus simple pour simuler une requête effectuée par un utilisateur authentifié. Vous pouvez spécifier le nom d'utilisateur, les rôles ou les autorités.

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
// ... autres imports

@Test
@WithMockUser // Simule un utilisateur simple ('user', 'password', 'ROLE_USER')
void accessProtectedResource_whenAuthenticated_shouldReturnOk() throws Exception {
    mockMvc.perform(get("/api/user/profile"))
           .andExpect(status().isOk());
}

@Test
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
void accessAdminResource_whenUserIsAdmin_shouldReturnOk() throws Exception {
    mockMvc.perform(get("/api/admin/dashboard"))
           .andExpect(status().isOk());
}

@Test
@WithMockUser(username = "basic_user", roles = {"USER"})
void accessAdminResource_whenUserIsNotAdmin_shouldReturnForbidden() throws Exception {
    mockMvc.perform(get("/api/admin/dashboard"))
           .andExpect(status().isForbidden()); // HTTP 403
}

Tests avec un utilisateur anonyme (`@WithAnonymousUser`) :

Pour vérifier l'accès aux ressources publiques ou pour s'assurer que les ressources protégées rejettent bien les utilisateurs non authentifiés, utilisez `@WithAnonymousUser`.

import org.springframework.security.test.context.support.WithAnonymousUser;
// ... autres imports

@Test
@WithAnonymousUser
void accessPublicResource_whenAnonymous_shouldReturnOk() throws Exception {
    mockMvc.perform(get("/api/public/info"))
           .andExpect(status().isOk());
}

@Test
@WithAnonymousUser
void accessProtectedResource_whenAnonymous_shouldReturnUnauthorized() throws Exception {
    // Si une authentification est requise (point d'entrée)
    mockMvc.perform(get("/api/user/profile"))
           .andExpect(status().isUnauthorized()); // HTTP 401
    // Ou isForbidden() (403) si l'anonymat est explicitement interdit mais pas d'invite à s'authentifier
}

Tester la sécurité au niveau méthode (@PreAuthorize, etc.)

Si vous utilisez des annotations comme `@PreAuthorize`, `@PostAuthorize`, `@Secured` sur vos méthodes de service ou de composant, vous pouvez également les tester.

Ces tests nécessitent que le contexte Spring soit chargé (`@SpringBootTest`) pour que l'AOP de sécurité soit actif. Vous utiliserez à nouveau des annotations comme `@WithMockUser` ou `@WithUserDetails` pour fournir le contexte de sécurité nécessaire à l'évaluation des expressions ou des rôles dans les annotations de sécurité.

Vous pouvez injecter directement le service que vous souhaitez tester et appeler la méthode sécurisée. Si l'accès doit être refusé, une exception comme `AccessDeniedException` sera généralement levée.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import static org.junit.jupiter.api.Assertions.*;
// ... autres imports

// Supposons un service avec une méthode sécurisée :
// @Service
// public class MySecuredService {
//     @PreAuthorize("hasRole('ADMIN')")
//     public String performAdminAction() { return "Admin action done"; }
//     @PreAuthorize("isAuthenticated()")
//     public String performUserAction() { return "User action done"; }
// }

@Autowired
private MySecuredService mySecuredService;

@Test
@WithMockUser(roles = "ADMIN")
void performAdminAction_whenUserIsAdmin_shouldSucceed() {
    String result = mySecuredService.performAdminAction();
    assertEquals("Admin action done", result);
}

@Test
@WithMockUser(roles = "USER")
void performAdminAction_whenUserIsNotAdmin_shouldThrowAccessDenied() {
    assertThrows(AccessDeniedException.class, () -> {
        mySecuredService.performAdminAction();
    });
}

@Test
@WithAnonymousUser
void performUserAction_whenAnonymous_shouldThrowAuthenticationCredentialsNotFoundException() {
    // Ou une AccessDeniedException selon la configuration exacte
    assertThrows(Exception.class, () -> { // Soyez plus spécifique si possible
        mySecuredService.performUserAction();
    });
}

Utilisation programmatique : SecurityMockMvcRequestPostProcessors

Plutôt que d'utiliser des annotations sur la méthode de test, vous pouvez définir le contexte de sécurité directement lors de la construction de la requête `MockMvc` en utilisant les `SecurityMockMvcRequestPostProcessors`. C'est utile si vous avez besoin de plus de contrôle ou si vous voulez tester différentes identités au sein de la même méthode de test.

Les méthodes statiques clés incluent `user()`, `anonymous()`, `csrf()`, `httpBasic()`, etc.

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
// ... autres imports

@Test
void testAccessWithDifferentUsers() throws Exception {
    // Test avec un utilisateur anonyme
    mockMvc.perform(get("/api/user/profile").with(anonymous()))
           .andExpect(status().isUnauthorized());

    // Test avec un utilisateur standard
    mockMvc.perform(get("/api/user/profile").with(user("testUser").roles("USER")))
           .andExpect(status().isOk());

    // Test avec un admin pour une ressource admin
    mockMvc.perform(get("/api/admin/dashboard").with(user("adminUser").roles("ADMIN")))
           .andExpect(status().isOk());

    // Test avec un utilisateur standard pour une ressource admin
    mockMvc.perform(get("/api/admin/dashboard").with(user("testUser").roles("USER")))
           .andExpect(status().isForbidden());
}

@Test
void testPostRequestWithCsrf() throws Exception {
    // Pour les requêtes POST, PUT, DELETE, si CSRF est activé, il faut inclure le token
    mockMvc.perform(post("/api/resource/create")
                .with(user("creator").roles("EDITOR"))
                .with(csrf()) // Ajoute un token CSRF valide
                .contentType("application/json")
                .content("{ \"name\": \"newData\" }"))
           .andExpect(status().isCreated());
}

Assertions sur l'état de sécurité : SecurityMockMvcResultMatchers

En plus de vérifier les codes de statut HTTP, vous pouvez faire des assertions plus spécifiques sur l'état de sécurité *après* l'exécution de la requête en utilisant les `SecurityMockMvcResultMatchers`.

Cela vous permet de vérifier si un utilisateur est authentifié, non authentifié, ou même de vérifier les détails de l'utilisateur authentifié.

import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
// ... autres imports

@Test
@WithMockUser(username="alice", roles="VIEWER")
void checkAuthenticationState_afterRequest() throws Exception {
    mockMvc.perform(get("/api/some/resource"))
           .andExpect(status().isOk()) // D'abord, vérifier le statut HTTP
           .andExpect(authenticated() // Vérifier que l'utilisateur est authentifié
                .withUsername("alice")
                .withRoles("VIEWER"));
}

@Test
@WithAnonymousUser
void checkUnauthenticatedState_afterRequest() throws Exception {
    mockMvc.perform(get("/api/public/info"))
           .andExpect(status().isOk())
           .andExpect(unauthenticated()); // Vérifier que l'utilisateur n'est pas authentifié
}

Tests plus réalistes avec @WithUserDetails

Bien que `@WithMockUser` soit pratique, il crée un utilisateur simulé qui n'existe pas nécessairement dans votre système réel. Pour des tests d'intégration plus poussés, vous voudrez peut-être exécuter des requêtes en tant qu'utilisateur chargé via votre propre implémentation de `UserDetailsService`.

L'annotation `@WithUserDetails` permet cela. Elle prend en argument le nom d'utilisateur d'un utilisateur qui peut être chargé par le bean `UserDetailsService` présent dans votre contexte de test.

import org.springframework.security.test.context.support.WithUserDetails;
// ... autres imports

// Prérequis : Avoir un bean UserDetailsService dans le contexte qui peut charger 'existingUser'
// et que cet utilisateur ait les bons droits (ex: ROLE_EDITOR)

@Test
@WithUserDetails("existingUser") // Charge l'utilisateur via UserDetailsService
void accessEditorResource_withRealUserDetails_shouldReturnOk() throws Exception {
    mockMvc.perform(get("/api/editor/content"))
           .andExpect(status().isOk());
}

@Test
@WithUserDetails(value="anotherUser", userDetailsServiceBeanName="myCustomUserDetailsService")
void accessResource_withCustomUserDetailsService_shouldReturnOk() throws Exception {
    // Si vous avez plusieurs beans UserDetailsService, spécifiez lequel utiliser
    mockMvc.perform(get("/api/some/other"))
           .andExpect(status().isOk());
}

Bonnes pratiques pour les tests de sécurité

Pour garantir une couverture de test efficace pour la sécurité de votre application Spring Boot :

  • Tester les cas positifs et négatifs : Vérifiez que les utilisateurs autorisés peuvent accéder et que les utilisateurs non autorisés sont bloqués.
  • Tester différents rôles/autorités : Si votre application utilise plusieurs rôles, testez l'accès avec chacun d'eux pour les ressources concernées.
  • Tester l'accès anonyme : Vérifiez explicitement le comportement pour les utilisateurs non authentifiés sur les endpoints publics et protégés.
  • Combiner tests web et méthode : Assurez-vous que vos tests couvrent à la fois les règles définies dans `HttpSecurity` et les annotations de sécurité au niveau des méthodes si vous les utilisez.
  • Tester les endpoints CSRF : Si la protection CSRF est activée (par défaut pour les navigateurs), n'oubliez pas d'inclure `.with(csrf())` dans vos tests pour les requêtes modifiant l'état (POST, PUT, DELETE, PATCH).
  • Utiliser `@WithUserDetails` pour des scénarios clés : Pour les tests d'intégration les plus importants, utilisez `@WithUserDetails` pour vous rapprocher des conditions réelles en utilisant votre logique de chargement d'utilisateur.
  • Garder les tests clairs et ciblés : Chaque méthode de test devrait idéalement vérifier un aspect spécifique de la sécurité.

En intégrant ces tests dans votre processus de développement, vous augmentez considérablement la confiance dans la robustesse de la sécurité de votre application Spring Boot.