SpringBoot
Tutorial SpringBoot: Testing de seguridad Spring Security
Programa pruebas de software para testing de controladores seguros con Spring Security, autenticación de formulario, autenticación JWT y OAuth cómo probar la seguridad.
Aprende SpringBoot GRATIS y certifícateTesting de controladores MVC con autenticación por formulario
La autenticación basada en formulario es una práctica común en aplicaciones web desarrolladas con Spring Boot y Thymeleaf. Para garantizar que esta funcionalidad opere correctamente, es esencial realizar testing de integración que verifique tanto el proceso de autenticación como el comportamiento de los controladores MVC involucrados.
Para comenzar, es necesario configurar el entorno de pruebas adecuadamente. Esto implica utilizar la anotación @SpringBootTest
para cargar el contexto completo de la aplicación y @AutoConfigureMockMvc
para inyectar una instancia de MockMvc
, la cual permite simular peticiones HTTP en los tests.
@SpringBootTest
@AutoConfigureMockMvc
public class AutenticacionFormularioTest {
@Autowired
private MockMvc mockMvc;
// ...
}
Con MockMvc
, es posible realizar peticiones POST al endpoint de inicio de sesión y verificar si la autenticación es exitosa. Por ejemplo, para probar que un usuario con credenciales correctas puede autenticarse y acceder a una página protegida:
@Test
void whenUserIsAuthenticated_thenAccessProtectedPage() throws Exception {
mockMvc.perform(formLogin("/login")
.user("username", "usuario")
.password("password", "contraseña"))
.andExpect(authenticated())
.andExpect(redirectedUrl("/pagina-protegida"));
}
En este ejemplo, se utiliza el método formLogin()
para simular el envío del formulario de autenticación al endpoint /login
. Los métodos user()
y password()
establecen las credenciales a utilizar. La aserción authenticated()
verifica que la autenticación haya sido exitosa, y redirectedUrl()
comprueba que el usuario sea redirigido a la URL esperada.
Es importante también probar el comportamiento ante credenciales incorrectas. Para ello, se puede escribir un test que valide que el acceso es denegado y que se muestra el mensaje de error correspondiente:
@Test
void whenUserIsNotAuthenticated_thenAccessDenied() throws Exception {
mockMvc.perform(formLogin("/login")
.user("username", "usuario")
.password("password", "contraseña-incorrecta"))
.andExpect(unauthenticated())
.andExpect(redirectedUrl("/login?error"));
}
Para verificar el acceso a controladores protegidos, se pueden realizar peticiones GET y comprobar que los usuarios autenticados pueden acceder mientras que los no autenticados son redirigidos al formulario de login:
@Test
void whenAccessProtectedUrlWithoutAuth_thenRedirectToLogin() throws Exception {
mockMvc.perform(get("/pagina-protegida"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
void whenAccessProtectedUrlWithAuth_thenSuccess() throws Exception {
mockMvc.perform(get("/pagina-protegida")
.with(user("usuario").password("contraseña").roles("USER")))
.andExpect(status().isOk())
.andExpect(view().name("paginaProtegida"));
}
En el segundo test, se utiliza el método with()
junto con user()
para establecer un usuario autenticado en el contexto de la petición. De esta forma, se simula el acceso de un usuario ya autenticado a la página protegida.
Al trabajar con Thymeleaf, es fundamental asegurarse de que las vistas renderizadas contengan los elementos esperados. Por ejemplo, se puede verificar que el nombre de usuario se muestra correctamente en la interfaz:
@Test
void whenUserIsAuthenticated_thenUsernameDisplayed() throws Exception {
mockMvc.perform(get("/pagina-protegida")
.with(user("usuario").password("contraseña").roles("USER")))
.andExpect(status().isOk())
.andExpect(xpath("//span[@id='username']").string("usuario"));
}
Este test utiliza expresiones XPath para navegar por el árbol DOM de la vista renderizada y comprobar que el elemento con id='username'
contiene el nombre de usuario esperado.
Es recomendable también probar el cierre de sesión (logout) para garantizar que la sesión del usuario se invalida correctamente:
@Test
void whenUserLogsOut_thenSessionInvalidated() throws Exception {
mockMvc.perform(post("/logout")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(unauthenticated())
.andExpect(redirectedUrl("/login?logout"));
}
Aquí, el método post()
envía una petición al endpoint de logout. La inclusión de with(csrf())
es necesaria ya que Spring Security por defecto exige un token CSRF en las peticiones que modifican el estado. La aserción unauthenticated()
verifica que el contexto de seguridad ya no contiene al usuario.
Finalmente, es esencial manejar correctamente los tokens CSRF en las pruebas. Si la configuración de seguridad incluye protección CSRF, las peticiones POST, PUT, DELETE, etc., deben incluir el token correspondiente:
@Test
void whenPostWithoutCsrf_thenForbidden() throws Exception {
mockMvc.perform(post("/accion-protegida"))
.andExpect(status().isForbidden());
}
Para incluir el token CSRF en las peticiones, se utiliza el método with(csrf())
:
@Test
void whenPostWithCsrf_thenSuccess() throws Exception {
mockMvc.perform(post("/accion-protegida")
.with(csrf())
.with(user("usuario").password("contraseña").roles("ADMIN")))
.andExpect(status().isOk());
}
Con estos enfoques, es posible realizar un testing integral de la autenticación basada en formulario en aplicaciones Spring Boot con controladores MVC y Thymeleaf, asegurando que tanto la lógica de seguridad como las interfaces de usuario funcionan según lo esperado.
Uso de anotaciones @WithMockUser y @WithUserDetails
Las pruebas de seguridad son esenciales para garantizar el correcto funcionamiento de una aplicación Spring Boot. Las anotaciones @WithMockUser y @WithUserDetails proporcionan una forma eficaz de simular contextos de seguridad en los tests, facilitando la verificación de roles, permisos y autenticaciones.
La anotación @WithMockUser permite simular un usuario autenticado con especificaciones predeterminadas o personalizadas. Por defecto, crea un usuario con el nombre "user", sin contraseña, y con el rol "ROLE_USER". Esta herramienta es especialmente útil para pruebas en las que se requiere un usuario autenticado sin necesidad de configurar un usuario real en la base de datos.
Por ejemplo, para probar un controlador que requiere autenticación, se puede utilizar:
@Test
@WithMockUser
void dadoUsuarioAutenticado_cuandoAccedeAEndpointProtegido_entoncesOk() throws Exception {
mockMvc.perform(get("/api/protegido"))
.andExpect(status().isOk());
}
En este caso, el test verifica que un usuario autenticado puede acceder al endpoint /api/protegido sin problemas. Si se necesita especificar detalles adicionales, como el nombre de usuario o los roles, se pueden proporcionar en la anotación:
@Test
@WithMockUser(username = "juan", roles = {"USER", "ADMIN"})
void dadoUsuarioAdmin_cuandoAccedeAEndpointAdmin_entoncesOk() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
Aquí, se simula un usuario llamado "juan" con los roles "USER" y "ADMIN". Esto es útil para probar rutas que requieren autorizaciones específicas.
Por otro lado, la anotación @WithUserDetails permite cargar un usuario real desde un UserDetailsService definido en la aplicación. Esta opción es adecuada cuando se necesita probar con usuarios que existen en la base de datos y se quiere verificar el comportamiento real del sistema de autenticación.
Para utilizar @WithUserDetails, primero es necesario asegurarse de que el UserDetailsService está correctamente configurado y disponible en el contexto de pruebas. Por ejemplo:
@Service
public class MiUserDetailsService implements UserDetailsService {
@Autowired
private UsuarioRepository usuarioRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return usuarioRepository.findByUsername(username)
.map(usuario -> new User(usuario.getUsername(), usuario.getPassword(), Collections.emptyList()))
.orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado"));
}
}
Luego, en el test, se utiliza @WithUserDetails especificando el nombre de usuario que se cargará:
@Test
@WithUserDetails("maria")
void dadoUsuarioExistente_cuandoAccedeAPerfil_entoncesOk() throws Exception {
mockMvc.perform(get("/api/perfil"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("maria"));
}
En este ejemplo, se carga el usuario "maria" desde el repositorio y se verifica que puede acceder a su perfil correctamente. Al utilizar @WithUserDetails, se está probando la integración completa del sistema de autenticación, lo que proporciona pruebas más realistas.
Para situaciones donde se requiere configurar el UserDetailsService específico para pruebas, se puede utilizar la anotación @TestConfiguration para crear una configuración de prueba:
@TestConfiguration
static class TestSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return username -> {
if ("pedro".equals(username)) {
return User.builder()
.username("pedro")
.password("{noop}password")
.roles("USER")
.build();
}
throw new UsernameNotFoundException("Usuario no encontrado");
};
}
}
De esta manera, durante las pruebas, el UserDetailsService utilizará esta configuración personalizada en lugar de la configuración de la aplicación principal.
Al combinar @WithMockUser y @WithUserDetails con MockMvc, se pueden realizar pruebas exhaustivas de seguridad. Por ejemplo, para comprobar que un usuario sin los permisos adecuados recibe un error de acceso denegado:
@Test
@WithMockUser(roles = "USER")
void dadoUsuarioConRolUser_cuandoAccedeAEndpointAdmin_entoncesForbidden() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isForbidden());
}
Aquí, un usuario con el rol "USER" intenta acceder a un endpoint que requiere el rol "ADMIN", y se verifica que el sistema responde con 403 Forbidden. Es fundamental en las pruebas de seguridad confirmar que las restricciones de acceso funcionan correctamente.
En casos donde se necesita probar interacciones más complejas, como cambios en el contexto de seguridad durante la ejecución, se puede utilizar el SecurityContextTestExecutionListener. Sin embargo, en la mayoría de las pruebas, @WithMockUser y @WithUserDetails son suficientes y proporcionan una manera sencilla y eficaz de simular diferentes escenarios de autenticación y autorización.
Además, es posible personalizar aún más los detalles del usuario simulado, incluyendo atributos como la contraseña o las autoridades. Por ejemplo:
@Test
@WithMockUser(username = "lucia", password = "secreto", authorities = {"READ_PRIVILEGES", "WRITE_PRIVILEGES"})
void dadoUsuarioConPrivilegios_cuandoRealizaAccion_entoncesOk() throws Exception {
mockMvc.perform(post("/api/accion")
.with(csrf()))
.andExpect(status().isOk());
}
Es importante notar que al incluir operaciones que modifican el estado, como un POST, se debe considerar el uso de tokens CSRF. La inclusión de with(csrf()) en el test asegura que el token CSRF se maneje correctamente, evitando errores de 403 Forbidden debidos a la protección CSRF de Spring Security.
Para pruebas unitarias de métodos que dependen del contexto de seguridad, se puede utilizar SecurityContextHolder para establecer manualmente la autenticación. Sin embargo, el uso de @WithMockUser simplifica este proceso y hace que las pruebas sean más claras y manejables.
Testing de controladores REST con JWT
La autenticación mediante JWT (JSON Web Tokens) se ha convertido en un estándar para asegurar API RESTful en aplicaciones modernas. Al desarrollar aplicaciones con Spring Boot 3, es crucial garantizar que los controladores REST funcionan correctamente bajo las capas de seguridad implementadas. En esta sección, exploraremos cómo realizar pruebas integradas de controladores REST protegidos con autenticación JWT, utilizando JUnit 5 y las herramientas proporcionadas por Spring Boot.
Para comenzar, es necesario configurar el entorno de pruebas para soportar la seguridad basada en JWT. Utilizaremos la anotación @SpringBootTest
para cargar el contexto completo de la aplicación y @AutoConfigureMockMvc
para inyectar una instancia de MockMvc
, que nos permitirá simular peticiones HTTP en los tests.
@SpringBootTest
@AutoConfigureMockMvc
public class ControladorRestJwtTest {
@Autowired
private MockMvc mockMvc;
// ...
}
Uno de los desafíos al probar controladores protegidos con JWT es la generación y gestión de tokens válidos. Para simular un usuario autenticado, podemos generar un token JWT dentro del test utilizando el mismo mecanismo que la aplicación en producción.
Supongamos que tenemos un servicio que se encarga de generar tokens JWT:
@Service
public class JwtTokenService {
// Método para generar un token JWT
public String generateToken(Authentication authentication) {
// Implementación de generación de token
}
// ...
}
En nuestro test, inyectaremos este servicio para generar un token válido:
@Autowired
private JwtTokenService jwtTokenService;
A continuación, podemos crear un método auxiliar para obtener un token JWT válido para un usuario determinado:
private String obtenerTokenAutorizacion(String username) {
UserDetails userDetails = User.withUsername(username)
.password("password")
.roles("USER")
.build();
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
return jwtTokenService.generateToken(authentication);
}
Con este token, podemos realizar peticiones autenticadas a los controladores REST protegidos:
@Test
void cuandoUsuarioAutenticadoAccedeAEndpointProtegido_entoncesRetornaOk() throws Exception {
String token = obtenerTokenAutorizacion("usuario");
mockMvc.perform(get("/api/protegido")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.mensaje").value("Acceso concedido"));
}
Es importante incluir el encabezado Authorization con el prefijo Bearer seguido del token JWT, tal como lo haría un cliente real. De esta manera, el filtro de seguridad de Spring Security procesará el token y autenticará la solicitud.
También es fundamental probar que los usuarios no autenticados o con tokens inválidos no pueden acceder a los endpoints protegidos:
@Test
void cuandoUsuarioNoAutenticadoAccedeAEndpointProtegido_entoncesRetornaUnauthorized() throws Exception {
mockMvc.perform(get("/api/protegido"))
.andExpect(status().isUnauthorized());
}
@Test
void cuandoTokenInvalido_entoncesRetornaUnauthorized() throws Exception {
String tokenInvalido = "token.no.valido";
mockMvc.perform(get("/api/protegido")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenInvalido))
.andExpect(status().isUnauthorized());
}
Para controlar el tiempo de expiración y otros detalles del token, podemos parametrizar el método de generación de tokens en los tests. Así, podemos probar escenarios donde el token esté expirado o tenga reclamos (claims) específicos.
Además, es posible que ciertos endpoints requieran roles o permisos específicos. Podemos verificar este comportamiento generando tokens con diferentes roles:
private String obtenerTokenAutorizacionConRol(String username, String rol) {
UserDetails userDetails = User.withUsername(username)
.password("password")
.roles(rol)
.build();
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
return jwtTokenService.generateToken(authentication);
}
@Test
void cuandoUsuarioConRolAdminAccedeAEndpointAdmin_entoncesRetornaOk() throws Exception {
String token = obtenerTokenAutorizacionConRol("admin", "ADMIN");
mockMvc.perform(get("/api/admin")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.mensaje").value("Acceso admin concedido"));
}
@Test
void cuandoUsuarioSinRolAdminAccedeAEndpointAdmin_entoncesRetornaForbidden() throws Exception {
String token = obtenerTokenAutorizacionConRol("usuario", "USER");
mockMvc.perform(get("/api/admin")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
.andExpect(status().isForbidden());
}
En estos tests, comprobamos que solo los usuarios con rol ADMIN pueden acceder al endpoint /api/admin, mientras que los usuarios con rol USER reciben un Forbidden. Esto nos permite validar las configuraciones de autorización basadas en roles.
Para manejar de manera más elegante la generación de tokens en los tests, podemos utilizar una clase utilitaria o un Test Configuration:
@TestConfiguration
static class JwtTestConfig {
@Bean
public JwtTokenService jwtTokenService() {
return new JwtTokenService() {
@Override
public String generateToken(Authentication authentication) {
// Implementación de generación de token para pruebas
}
};
}
}
De esta forma, podemos mockear o ajustar el comportamiento del servicio de tokens para las pruebas, sin afectar la funcionalidad en producción.
Es igualmente importante probar la autenticación en sí misma, es decir, el proceso de login donde el usuario obtiene el token JWT:
@Test
void cuandoCredencialesValidas_entoncesRetornaToken() throws Exception {
Map<String, String> credenciales = new HashMap<>();
credenciales.put("username", "usuario");
credenciales.put("password", "password");
mockMvc.perform(post("/api/login")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(credenciales)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists());
}
@Test
void cuandoCredencialesInvalidas_entoncesRetornaUnauthorized() throws Exception {
Map<String, String> credenciales = new HashMap<>();
credenciales.put("username", "usuario");
credenciales.put("password", "passwordIncorrecta");
mockMvc.perform(post("/api/login")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(credenciales)))
.andExpect(status().isUnauthorized());
}
En estos ejemplos, simulamos el proceso de inicio de sesión enviando las credenciales del usuario y verificamos que se devuelve un token cuando las credenciales son válidas, o un código de estado Unauthorized cuando no lo son.
Para mejorar la legibilidad y mantenimiento de los tests, es recomendable utilizar constantes para las rutas y mensajes, y posiblemente utilizar Builder patterns o métodos utilitarios para configurar las peticiones.
Al probar controladores REST con JWT, es esencial también considerar aspectos como el refresco de tokens, manejo de tokens revocados y multi-factor authentication si la aplicación lo requiere. Aunque estos escenarios pueden ser más complejos, su inclusión en las pruebas garantizará una mayor robustez y seguridad en la aplicación.
Finalmente, es importante asegurarse de que el contexto de seguridad en los tests es limpio en cada ejecución. Para ello, podemos utilizar la anotación @WithAnonymousUser
o asegurarnos de que no quedan autenticaciones previas que puedan afectar los resultados.
Al integrar estas prácticas de testing, estaremos validando no solo los controladores REST, sino también todo el flujo de seguridad basado en JWT, lo cual es fundamental para aplicaciones modernas y seguras.
Testing de controladores REST reactivos con JWT
El desarrollo de aplicaciones reactivas con Spring WebFlux y la seguridad mediante JWT (JSON Web Tokens) son tendencias clave en la construcción de sistemas modernos y escalables. Garantizar que los controladores REST reactivos funcionen correctamente bajo estas capas de seguridad es esencial. En esta sección, abordaremos cómo realizar pruebas integradas de controladores REST reactivos protegidos con autenticación JWT, utilizando JUnit 5 y las herramientas proporcionadas por Spring Boot 3.
Para comenzar, es fundamental establecer un entorno de pruebas adecuado que soporte la naturaleza reactiva de WebFlux y la seguridad JWT. Utilizaremos la anotación @SpringBootTest
para cargar el contexto completo de la aplicación y @AutoConfigureWebTestClient
para inyectar una instancia de WebTestClient
, que permitirá simular y probar peticiones de manera reactiva.
@SpringBootTest
@AutoConfigureWebTestClient
public class ControladorRestReactivoJwtTest {
@Autowired
private WebTestClient webTestClient;
// ...
}
El WebTestClient es una herramienta diseñada específicamente para pruebas de aplicaciones reactivas, proporcionando una API fluida y reactiva para realizar y verificar peticiones HTTP.
Uno de los desafíos al probar controladores protegidos con JWT es manejar correctamente la generación y suministro de tokens válidos. Para simular un usuario autenticado, es necesario generar un token JWT que refleje los mismos estándares de seguridad que en producción.
Supongamos que disponemos de un servicio encargado de generar tokens JWT:
@Service
public class JwtUtil {
// Método para generar un token JWT
public String generarToken(UserDetails userDetails) {
// Implementación de generación de token
}
// ...
}
En nuestro test, inyectaremos este servicio para generar un token válido que pueda ser utilizado en las peticiones de prueba:
@Autowired
private JwtUtil jwtUtil;
Para simplificar, crearemos un método auxiliar que nos permita obtener un token JWT para un usuario específico:
private String obtenerTokenAutorizacion(String username) {
UserDetails userDetails = User.withUsername(username)
.password("password")
.roles("USER")
.build();
return jwtUtil.generarToken(userDetails);
}
Con este token, podemos realizar peticiones autenticadas a los controladores REST reactivos protegidos:
@Test
void dadoUsuarioAutenticado_cuandoAccedeARecursoProtegido_entoncesRetornaOk() {
String token = obtenerTokenAutorizacion("usuario");
webTestClient.get()
.uri("/api/reactivo/protegido")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.mensaje").isEqualTo("Acceso concedido");
}
Es esencial incluir el encabezado Authorization con el prefijo Bearer seguido del token JWT, simulando el comportamiento de un cliente real. De esta manera, el flujo de seguridad de Spring Security procesará el token y autenticará la solicitud de forma reactiva.
También es importante verificar que las solicitudes sin autenticación o con tokens inválidos sean rechazadas adecuadamente:
@Test
void dadoUsuarioNoAutenticado_cuandoAccedeARecursoProtegido_entoncesRetornaUnauthorized() {
webTestClient.get()
.uri("/api/reactivo/protegido")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
void dadoTokenInvalido_cuandoAccedeARecursoProtegido_entoncesRetornaUnauthorized() {
String tokenInvalido = "token.invalido";
webTestClient.get()
.uri("/api/reactivo/protegido")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenInvalido)
.exchange()
.expectStatus().isUnauthorized();
}
Para probar endpoints que requieren roles o permisos específicos, podemos generar tokens con diferentes roles e indagar cómo responde la aplicación:
private String obtenerTokenConRol(String username, String rol) {
UserDetails userDetails = User.withUsername(username)
.password("password")
.roles(rol)
.build();
return jwtUtil.generarToken(userDetails);
}
@Test
void dadoUsuarioConRolAdmin_cuandoAccedeARecursoAdmin_entoncesRetornaOk() {
String token = obtenerTokenConRol("admin", "ADMIN");
webTestClient.get()
.uri("/api/reactivo/admin")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.mensaje").isEqualTo("Acceso admin concedido");
}
@Test
void dadoUsuarioSinRolAdmin_cuandoAccedeARecursoAdmin_entoncesRetornaForbidden() {
String token = obtenerTokenConRol("usuario", "USER");
webTestClient.get()
.uri("/api/reactivo/admin")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isForbidden();
}
Estos tests confirman que el sistema de autorización está funcionando correctamente, permitiendo el acceso únicamente a los usuarios con el rol adecuado.
En aplicaciones reactivas, es crucial manejar correctamente los flujos de datos y las suscripciones. Por ello, al probar endpoints que devuelven Flux o Mono, debemos verificar no solo el status HTTP, sino también el contenido reactivo:
@Test
void dadoUsuarioAutenticado_cuandoObtieneListadoReactivo_entoncesRecibeDatos() {
String token = obtenerTokenAutorizacion("usuario");
webTestClient.get()
.uri("/api/reactivo/listado")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isOk()
.expectBodyList(Objeto.class)
.hasSize(5)
.consumeWith(response -> {
List<Objeto> objetos = response.getResponseBody();
// Asersiones adicionales sobre los objetos recibidos
});
}
En este ejemplo, verificamos que el endpoint devuelve una lista reactiva de objetos, y podemos realizar asersiones sobre el contenido recibido.
Para probar el proceso de autenticación en sí mismo, es decir, la obtención del token JWT, también podemos simular el login y verificar que se genera el token correctamente:
@Test
void dadoCredencialesValidas_cuandoAutenticaReactivo_entoncesRecibeToken() {
Map<String, String> credenciales = new HashMap<>();
credenciales.put("username", "usuario");
credenciales.put("password", "password");
webTestClient.post()
.uri("/api/reactivo/login")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(credenciales))
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.token").isNotEmpty();
}
@Test
void dadoCredencialesInvalidas_cuandoAutenticaReactivo_entoncesRetornaUnauthorized() {
Map<String, String> credenciales = new HashMap<>();
credenciales.put("username", "usuario");
credenciales.put("password", "passwordIncorrecto");
webTestClient.post()
.uri("/api/reactivo/login")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(credenciales))
.exchange()
.expectStatus().isUnauthorized();
}
La verificación de la correcta generación y suministro del token JWT es esencial para asegurar que el flujo de autenticación funciona como se espera.
En ocasiones, es útil crear una configuración de seguridad específica para las pruebas, aislando el entorno de producción. Podemos utilizar la anotación @TestConfiguration
para definir beans y componentes que solo se aplicarán durante los tests:
@TestConfiguration
static class SeguridadTestConfig {
@Bean
public JwtUtil jwtUtil() {
return new JwtUtil() {
@Override
public String generarToken(UserDetails userDetails) {
// Implementación simplificada para pruebas
}
};
}
// Otros beans necesarios para las pruebas
}
Esta configuración permite personalizar el comportamiento de los componentes de seguridad sin afectar al entorno real de la aplicación.
Cuando trabajamos con controladores reactivos, es importante tener en cuenta el manejo de excepciones y errores. Podemos probar cómo la aplicación responde ante situaciones excepcionales:
@Test
void dadoErrorEnServidor_cuandoAccedeARecursoReactivo_entoncesRetornaError() {
String token = obtenerTokenAutorizacion("usuario");
webTestClient.get()
.uri("/api/reactivo/error")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().is5xxServerError()
.expectBody()
.jsonPath("$.error").isEqualTo("Error interno del servidor");
}
La validación de estos escenarios garantiza que la aplicación maneja adecuadamente las excepciones y proporciona respuestas coherentes al cliente.
Además, podemos aprovechar las características reactivas de WebTestClient para probar flujos más complejos, como Streams de datos o comunicaciones Server-Sent Events (SSE):
@Test
void dadoUsuarioAutenticado_cuandoRecibeEventosReactivos_entoncesProcesaEventos() {
String token = obtenerTokenAutorizacion("usuario");
FluxExchangeResult<ServerSentEvent<String>> result = webTestClient.get()
.uri("/api/reactivo/eventos")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.exchange()
.expectStatus().isOk()
.returnResult(new ParameterizedTypeReference<ServerSentEvent<String>>() {});
StepVerifier.create(result.getResponseBody())
.expectNextMatches(evento -> evento.data().equals("Evento 1"))
.expectNextMatches(evento -> evento.data().equals("Evento 2"))
.thenCancel()
.verify();
}
En este caso, utilizamos StepVerifier para suscribirnos al flujo de eventos y verificar que se reciben los datos esperados.
Finalmente, es importante asegurarse de que los tests no dejan contextos de seguridad residuales que puedan interferir con otras pruebas. Cada test debe ser autónomo y limpiar cualquier estado compartido. Podemos utilizar la anotación @DirtiesContext
si es necesario, aunque en la mayoría de casos, con un adecuado diseño de las pruebas, esto no será imprescindible.
Al incorporar estas prácticas de testing, estamos validando no solo la funcionalidad de los controladores REST reactivos, sino también la integridad del sistema de seguridad basado en JWT en un entorno reactivo. Esto es fundamental para aplicaciones que buscan aprovechar al máximo la capacidad de escalabilidad y rendimiento que ofrece Spring WebFlux en combinación con Spring Security.
Testing de controladores REST con OAuth 2
Al desarrollar APIs RESTful con Spring Boot 3, es habitual securizar los endpoints utilizando OAuth 2.0, permitiendo a los usuarios autenticarse a través de proveedores externos como GitHub. Para garantizar que los controladores funcionan correctamente bajo estas capas de seguridad, es esencial realizar pruebas integradas específicas. En esta sección, exploraremos cómo llevar a cabo testing de controladores REST de Spring Web que están protegidos con OAuth 2 utilizando GitHub como proveedor de autenticación.
Para comenzar, es necesario configurar el entorno de pruebas adecuadamente. Utilizaremos la anotación @SpringBootTest
para cargar el contexto completo de la aplicación y @AutoConfigureMockMvc
para inyectar una instancia de MockMvc
, que nos permitirá simular peticiones HTTP en los tests.
@SpringBootTest
@AutoConfigureMockMvc
public class ControladorRestOAuth2Test {
@Autowired
private MockMvc mockMvc;
// ...
}
Dado que nuestros controladores están protegidos con OAuth 2.0, necesitamos simular el proceso de autenticación con GitHub. Spring Security Test proporciona herramientas que facilitan esta tarea, permitiendo mockear la autenticación OAuth 2 en los tests. En particular, podemos utilizar el método oauth2Login()
de SecurityMockMvcRequestPostProcessors
para simular un usuario autenticado.
Por ejemplo, supongamos que tenemos un controlador protegido:
@RestController
@RequestMapping("/api")
public class RepositorioController {
@GetMapping("/repos")
public ResponseEntity<List<Repositorio>> obtenerRepositorios(OAuth2AuthenticationToken authentication) {
// Lógica para obtener repositorios del usuario autenticado
}
}
Para probar este controlador, podemos escribir un test que simule una petición autenticada:
@Test
void cuandoUsuarioAutenticadoAccedeARepositorios_entoncesRetornaOk() throws Exception {
mockMvc.perform(get("/api/repos")
.with(oauth2Login()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(5));
}
En este test, el método oauth2Login()
simula que un usuario está autenticado a través de OAuth 2. Por defecto, se crea un usuario con atributos estándar, pero podemos personalizarlo según nuestras necesidades.
Si queremos especificar detalles adicionales del usuario autenticado, como el nombre, el email o los scopes otorgados, podemos crear un objeto OAuth2User
personalizado:
@Test
void cuandoUsuarioConAlcanceLeecturaAccede_entoncesRetornaOk() throws Exception {
OAuth2User usuario = OAuth2UserAuthorityBuilder.builder()
.nameAttributeKey("id")
.authorities(new SimpleGrantedAuthority("SCOPE_read"))
.attributes(attrs -> {
attrs.put("id", "12345");
attrs.put("login", "usuarioGitHub");
attrs.put("email", "usuario@example.com");
})
.build();
mockMvc.perform(get("/api/repos")
.with(oauth2Login().oauth2User(usuario)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(5));
}
En este ejemplo, creamos un OAuth2User
con un atributo id
, un login
y un email
, además de asignarle el alcance (scope) read
. Esto permite simular situaciones específicas, como verificar que solo los usuarios con ciertos scopes pueden acceder a determinados recursos.
También es fundamental probar el comportamiento de la aplicación cuando un usuario no está autenticado o cuando no tiene los permisos adecuados. Por ejemplo:
@Test
void cuandoUsuarioNoAutenticadoAccedeARepositorios_entoncesRetornaRedireccion() throws Exception {
mockMvc.perform(get("/api/repos"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string("Location", containsString("https://github.com/login")));
}
Aquí, verificamos que un usuario no autenticado es redirigido a la página de login de GitHub al intentar acceder al endpoint protegido.
Si necesitamos probar el acceso a recursos que requieren scopes específicos, podemos simular diferentes escenarios variando los scopes del usuario:
@Test
void cuandoUsuarioSinAlcanceEscrituraAccedeAEndpointProtegido_entoncesRetornaForbidden() throws Exception {
OAuth2User usuario = OAuth2UserAuthorityBuilder.builder()
.authorities(new SimpleGrantedAuthority("SCOPE_read"))
.build();
mockMvc.perform(post("/api/repos")
.with(oauth2Login().oauth2User(usuario)))
.andExpect(status().isForbidden());
}
En este caso, un usuario con solo el scope read
intenta realizar una operación POST
que requiere el scope write
. El test verifica que la aplicación responde con un Forbidden, denegando el acceso.
Para gestionar la configuración de seguridad en los tests, es recomendable utilizar una clase de configuración específica para pruebas. Podemos definir un client registration simulado para GitHub:
@TestConfiguration
static class OAuth2TestConfig {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration registroGitHub = ClientRegistration
.withRegistrationId("github")
.clientId("clienteId")
.clientSecret("clienteSecret")
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.tokenUri("https://github.com/login/oauth/access_token")
.authorizationUri("https://github.com/login/oauth/authorize")
.userInfoUri("https://api.github.com/user")
.userNameAttributeName("id")
.clientName("GitHub")
.build();
return new InMemoryClientRegistrationRepository(registroGitHub);
}
}
Esta configuración permite simular el registro del cliente OAuth 2 para GitHub, evitando dependencias externas durante las pruebas.
Además, si necesitamos personalizar el token de acceso o añadir atributos al usuario, podemos utilizar OAuth2AccessToken
y OAuth2AuthenticatedPrincipal
:
@Test
void cuandoUsuarioAutenticadoConTokenPersonalizado_entoncesAccesoConcedido() throws Exception {
OAuth2AccessToken tokenAcceso = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
"token-personalizado",
Instant.now(),
Instant.now().plus(Duration.ofHours(1)),
Set.of("read", "write")
);
Map<String, Object> atributos = Map.of(
"id", "12345",
"login", "usuarioGitHub",
"email", "usuario@example.com"
);
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority("SCOPE_read"), new SimpleGrantedAuthority("SCOPE_write")),
atributos,
"login"
);
mockMvc.perform(get("/api/repos")
.with(oauth2Login()
.oauth2User(principal)
.accessToken(tokenAcceso)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(5));
}
Este enfoque nos permite simular tokens de acceso con scopes y atributos específicos, proporcionando un mayor control sobre el contexto de seguridad en los tests.
Es importante también probar la integración con los endpoints de autorización y token. Sin embargo, en entornos de prueba, no es práctico interactuar con GitHub directamente. Por ello, podemos mockear las respuestas de GitHub utilizando herramientas como Mockito o servidores simulados.
Por ejemplo, podemos mockear el OAuth2AuthorizedClientService
para retornar un token predefinido:
@MockBean
private OAuth2AuthorizedClientService authorizedClientService;
@BeforeEach
void setup() {
OAuth2AuthorizedClient clientAutorizado = new OAuth2AuthorizedClient(
clientRegistrationRepository.findByRegistrationId("github"),
"usuarioGitHub",
new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
"token-simulado",
Instant.now(),
Instant.now().plus(Duration.ofHours(1)),
Set.of("read", "write")
)
);
when(authorizedClientService.loadAuthorizedClient(any(), any()))
.thenReturn(clientAutorizado);
}
De esta manera, al ejecutar los tests, la aplicación utilizará el cliente autorizado simulado, permitiendo probar el comportamiento sin realizar llamadas externas.
Otro aspecto crucial es verificar que la aplicación maneja adecuadamente los errores de autenticación y autorización. Podemos simular situaciones donde el token ha expirado o no es válido:
@Test
void cuandoTokenExpirado_entoncesRetornaUnauthorized() throws Exception {
OAuth2AccessToken tokenExpirado = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
"token-expirado",
Instant.now().minus(Duration.ofHours(2)),
Instant.now().minus(Duration.ofHours(1)),
Set.of("read")
);
mockMvc.perform(get("/api/repos")
.with(oauth2Login().accessToken(tokenExpirado)))
.andExpect(status().isUnauthorized());
}
Este test verifica que la aplicación responde con un Unauthorized cuando el token de acceso ha expirado.
Para mejorar la legibilidad y reutilización del código en los tests, es recomendable crear métodos auxiliares o builders para configurar el usuario y el token de manera más sencilla:
private RequestPostProcessor usuarioOAuth2(String login, Set<String> scopes) {
OAuth2AccessToken tokenAcceso = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
"token-" + login,
Instant.now(),
Instant.now().plus(Duration.ofHours(1)),
scopes
);
Map<String, Object> atributos = Map.of("login", login);
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2User(
scopes.stream()
.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toList()),
atributos,
"login"
);
return oauth2Login()
.oauth2User(principal)
.accessToken(tokenAcceso);
}
Con este método, podemos simplificar los tests:
@Test
void cuandoUsuarioConAlcanceEscrituraActualizaRepositorio_entoncesRetornaOk() throws Exception {
mockMvc.perform(put("/api/repos/1")
.with(usuarioOAuth2("usuarioGitHub", Set.of("write"))))
.andExpect(status().isOk());
}
Esta práctica mejora la claridad de los tests y facilita su mantenimiento.
Finalmente, es esencial asegurarse de que el contexto de seguridad está limpio en cada ejecución de los tests, evitando influencias entre ellos. JUnit 5 y Spring Test se encargan de gestionar el contexto de forma aislada, pero es buena práctica verificar que no queden autenticaciones previas que puedan afectar los resultados.
Al integrar estas técnicas de testing, estamos validando no solo los controladores REST, sino también todo el flujo de seguridad basado en OAuth 2 con GitHub, lo cual es fundamental para aplicaciones que dependen de proveedores externos de autenticación.
Ejercicios de esta lección Testing de seguridad Spring Security
Evalúa tus conocimientos de esta lección Testing de seguridad Spring Security con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
API Query By Example (QBE)
Identificadores y relaciones JPA
Borrar datos de base de datos
Web y Test Starters
Métodos find en repositorios
Controladores Spring MVC
Inserción de datos
CRUD Customers Spring MVC + Spring Data JPA
Backend API REST con Spring Boot
Controladores Spring REST
Uso de Spring con Thymeleaf
API Specification
Registro de usuarios
Crear entidades JPA
Asociaciones en JPA
Asociaciones de entidades JPA
Integración con Vue
Consultas JPQL
Open API y cómo agregarlo en Spring Boot
Uso de Controladores REST
Repositorios reactivos
Inyección de dependencias
Introducción a Spring Boot
CRUD y JPA Repository
Inyección de dependencias
Vista en Spring MVC con Thymeleaf
Servicios en Spring
Operadores Reactivos
Configuración de Vue
Entidades JPA
Integración con Angular
API Specification
API Query By Example (QBE)
Controladores MVC
Anotaciones y mapeo en JPA
Consultas JPQL con @Query en Spring Data JPA
Repositorios Spring Data
Inyección de dependencias
Data JPA y Mail Starters
Configuración de Angular
Controladores Spring REST
Configuración de Controladores MVC
Consultas JPQL con @Query en Spring Data JPA
Actualizar datos de base de datos
Verificar token JWT en peticiones
Login de usuarios
Integración con React
Configuración de React
Todas las lecciones de SpringBoot
Accede a todas las lecciones de SpringBoot y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Spring Boot
Introducción Y Entorno
Spring Boot Starters
Introducción Y Entorno
Inyección De Dependencias
Introducción Y Entorno
Controladores Spring Mvc
Spring Web
Vista En Spring Mvc Con Thymeleaf
Spring Web
Controladores Spring Rest
Spring Web
Open Api Y Cómo Agregarlo En Spring Boot
Spring Web
Servicios En Spring
Spring Web
Clientes Resttemplate Y Restclient
Spring Web
Rxjava En Spring Web
Spring Web
Crear Entidades Jpa
Persistencia Spring Data
Asociaciones De Entidades Jpa
Persistencia Spring Data
Repositorios Spring Data
Persistencia Spring Data
Métodos Find En Repositorios
Persistencia Spring Data
Inserción De Datos
Persistencia Spring Data
Actualizar Datos De Base De Datos
Persistencia Spring Data
Borrar Datos De Base De Datos
Persistencia Spring Data
Consultas Jpql Con @Query En Spring Data Jpa
Persistencia Spring Data
Api Query By Example (Qbe)
Persistencia Spring Data
Api Specification
Persistencia Spring Data
Repositorios Reactivos
Persistencia Spring Data
Introducción E Instalación De Apache Kafka
Mensajería Asíncrona
Crear Proyecto Con Apache Kafka
Mensajería Asíncrona
Creación De Producers
Mensajería Asíncrona
Creación De Consumers
Mensajería Asíncrona
Kafka Streams En Spring Boot
Mensajería Asíncrona
Introducción A Spring Webflux
Reactividad Webflux
Spring Data R2dbc
Reactividad Webflux
Controlador Rest Reactivo Basado En Anotaciones
Reactividad Webflux
Controlador Rest Reactivo Funcional
Reactividad Webflux
Operadores Reactivos Básicos
Reactividad Webflux
Operadores Reactivos Avanzados
Reactividad Webflux
Cliente Reactivo Webclient
Reactividad Webflux
Introducción A Spring Security
Seguridad Con Spring Security
Seguridad Basada En Formulario En Mvc Con Thymeleaf
Seguridad Con Spring Security
Registro De Usuarios
Seguridad Con Spring Security
Login De Usuarios
Seguridad Con Spring Security
Verificar Token Jwt En Peticiones
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Spring Web
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Reactiva Spring Webflux
Seguridad Con Spring Security
Autenticación Y Autorización Con Anotaciones
Seguridad Con Spring Security
Testing Unitario De Componentes Y Servicios
Testing Con Spring Test
Testing De Repositorios Spring Data Jpa
Testing Con Spring Test
Testing Controladores Spring Mvc Con Thymeleaf
Testing Con Spring Test
Testing Controladores Rest Con Json
Testing Con Spring Test
Testing De Aplicaciones Reactivas Webflux
Testing Con Spring Test
Testing De Seguridad Spring Security
Testing Con Spring Test
Testing Con Apache Kafka
Testing Con Spring Test
Integración Con Angular
Integración Frontend
Integración Con React
Integración Frontend
Integración Con Vue
Integración Frontend
En esta lección
Objetivos de aprendizaje de esta lección
- Aprender a probar controladores MVC securizados
- Aprender a usar @WithMockUser y @WithUserDetails
- Aprender a probar controladores REST json
- Aprender a probar controladores securizados por JWT y OAuth 2