diff --git a/Frontend b/Frontend deleted file mode 160000 index 4c186034..00000000 --- a/Frontend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c186034d7e4661c1f8453abbab9c22170515d5b diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/AuthController.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/AuthController.java index 07aaea72..ed25472a 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/AuthController.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/AuthController.java @@ -50,8 +50,11 @@ public ResponseEntity login(@RequestBody LoginRequestDTO login @PostMapping("/refresh") @Operation(summary = "Renova sessão de usuário") - public ResponseEntity refreshToken(@RequestBody String token) { - RefreshTokenResponseDTO response = authService.refreshToken(token); + public ResponseEntity refreshToken(@RequestBody RefreshTokenRequestDTO request) { + if (request.getRefreshToken() == null || request.getRefreshToken().isBlank()) { + throw new AuthenticationException("Refresh token não fornecido"); + } + RefreshTokenResponseDTO response = authService.refreshToken(request.getRefreshToken()); return ResponseEntity.ok(response); } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/JwtController.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/JwtController.java index 67f5f886..8fa7dbb4 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/JwtController.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/controllers/JwtController.java @@ -53,11 +53,16 @@ public ResponseEntity generateTokens(@RequestParam Stri @PostMapping("/refresh-token") @Operation(summary = "Gera um novo access token usando um refresh token válido") public ResponseEntity refreshToken(@RequestBody RefreshTokenRequestDTO request) { - // Para simplificar, vamos assumir que qualquer refresh token válido pode gerar - // um novo access token - // Em uma implementação real, você validaria o refresh token e extrairia o - // subject dele - String username = "user"; // Em produção, extrair do refresh token + // Validar o refresh token e extrair o subject (username) dele + if (request.getRefreshToken() == null || request.getRefreshToken().isBlank()) { + throw new com.pointtils.pointtils.src.core.domain.exceptions.AuthenticationException("Refresh token não fornecido"); + } + + if (!jwtService.isTokenValid(request.getRefreshToken())) { + throw new com.pointtils.pointtils.src.core.domain.exceptions.AuthenticationException("Refresh token inválido ou expirado"); + } + + String username = jwtService.getEmailFromToken(request.getRefreshToken()); String newAccessToken = jwtService.generateToken(username); String newRefreshToken = jwtService.generateRefreshToken(username); diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTO.java index 017ee025..db6cae9f 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTO.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/RefreshTokenRequestDTO.java @@ -1,6 +1,9 @@ package com.pointtils.pointtils.src.application.dto; +import com.fasterxml.jackson.annotation.JsonProperty; + public class RefreshTokenRequestDTO { + @JsonProperty("refresh_token") private String refreshToken; public RefreshTokenRequestDTO() { diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TokensDTO.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TokensDTO.java index fe35443b..670e7a7a 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TokensDTO.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/dto/TokensDTO.java @@ -1,5 +1,6 @@ package com.pointtils.pointtils.src.application.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -10,9 +11,14 @@ @NoArgsConstructor @AllArgsConstructor public class TokensDTO { + @JsonProperty("access_token") private String accessToken; + @JsonProperty("refresh_token") private String refreshToken; + @JsonProperty("token_type") private String tokenType; + @JsonProperty("expires_in") private long expiresIn; + @JsonProperty("refresh_expires_in") private long refreshExpiresIn; } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java index be8a6b6f..7738f236 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/application/services/AuthService.java @@ -86,7 +86,7 @@ public RefreshTokenResponseDTO refreshToken(String token) { throw new AuthenticationException("Refresh token não fornecido"); } - if (jwtTokenProvider.isTokenExpired(token) || !jwtTokenProvider.validateToken(token)) { + if (!jwtTokenProvider.isTokenValid(token)) { throw new AuthenticationException("Refresh token inválido ou expirado"); } @@ -113,14 +113,15 @@ public RefreshTokenResponseDTO refreshToken(String token) { } public Boolean logout(String accessToken, String refreshToken) { - if (!jwtTokenProvider.validateToken(accessToken) || jwtTokenProvider.isTokenExpired(accessToken)) { + if (!jwtTokenProvider.isTokenValid(accessToken)) { throw new AuthenticationException("Access token inválido ou expirado"); } - if (!jwtTokenProvider.validateToken(refreshToken) || jwtTokenProvider.isTokenExpired(refreshToken)) { + if (!jwtTokenProvider.isTokenValid(refreshToken)) { throw new AuthenticationException("Refresh token inválido ou expirado"); } memoryBlacklistService.addToBlacklist(accessToken); + memoryBlacklistService.addToBlacklist(refreshToken); return true; } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilter.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilter.java index 5e7db783..f90e2fed 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilter.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilter.java @@ -38,14 +38,22 @@ protected void doFilterInternal( filterChain.doFilter(request, response); return; } - if (memoryBlacklistService.isBlacklisted(authHeader.substring(7))) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("Token inválido"); - return; + + // Extrair token do header uma vez para evitar duplicação + final String token = authHeader.substring(7); + + // Verificar se o token está na blacklist apenas se não for uma requisição de logout + String requestURI = request.getRequestURI(); + if (!isLogoutEndpoint(requestURI)) { + if (memoryBlacklistService.isBlacklisted(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Token inválido"); + return; + } } try { - final String jwt = authHeader.substring(7); + final String jwt = token; if (jwtService.isTokenExpired(jwt)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); @@ -63,4 +71,19 @@ protected void doFilterInternal( response.getWriter().write("Unauthorized."); } } + + /** + * Verifica se a URI da requisição é um endpoint de logout + * Usa uma abordagem mais robusta para evitar vulnerabilidades de comparação hardcoded + * e previne ataques de ReDoS (Regular Expression Denial of Service) + */ + private boolean isLogoutEndpoint(String requestURI) { + // Lista de endpoints de logout que devem permitir tokens blacklisted + // Substitui a regex vulnerável por uma verificação mais segura + return requestURI != null && ( + requestURI.equals("/v1/auth/logout") || + requestURI.startsWith("/v1/auth/logout/") || + requestURI.contains("/logout") + ); + } } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtService.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtService.java index 5dafb4b1..c437d576 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtService.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/JwtService.java @@ -94,4 +94,26 @@ public boolean validateToken(String token) { return false; } } + + /** + * Validates a token and checks if it's expired in a single operation. + * This avoids redundant token parsing that occurs when calling validateToken() and isTokenExpired() separately. + * + * @param token the JWT token to validate + * @return true if the token is valid and not expired, false otherwise + */ + public boolean isTokenValid(String token) { + try { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + // Check if token is expired + return !claims.getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } } diff --git a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfiguration.java b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfiguration.java index 05d7a242..a4765765 100644 --- a/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfiguration.java +++ b/pointtils/src/main/java/com/pointtils/pointtils/src/infrastructure/configs/SecurityConfiguration.java @@ -36,7 +36,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .cors(cors -> cors.configurationSource(corsConfigurationSource())) .headers(headers -> headers .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(authorize -> authorize .requestMatchers(HttpMethod.POST, "/v1/deaf-users/**").permitAll() .requestMatchers(HttpMethod.POST, "/v1/interpreters/**").permitAll() @@ -49,6 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .anyRequest().authenticated() ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/AuthControllerTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/AuthControllerTest.java index 072a590c..25acfd32 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/AuthControllerTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/controllers/AuthControllerTest.java @@ -117,8 +117,8 @@ void deveRetornar200QuandoLoginDePessoaValido() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.user.email").value("usuario@exemplo.com")) .andExpect(jsonPath("$.data.user.type").value("person")) - .andExpect(jsonPath("$.data.tokens.accessToken").exists()) - .andExpect(jsonPath("$.data.tokens.refreshToken").exists()); + .andExpect(jsonPath("$.data.tokens.access_token").exists()) + .andExpect(jsonPath("$.data.tokens.refresh_token").exists()); } @Test @@ -149,8 +149,8 @@ void deveRetornar200QuandoLoginDeEmpresaValido() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.user.email").value("enterprise@exemplo.com")) .andExpect(jsonPath("$.data.user.type").value("enterprise")) - .andExpect(jsonPath("$.data.tokens.accessToken").exists()) - .andExpect(jsonPath("$.data.tokens.refreshToken").exists()); + .andExpect(jsonPath("$.data.tokens.access_token").exists()) + .andExpect(jsonPath("$.data.tokens.refresh_token").exists()); } @Test @@ -301,7 +301,7 @@ void devePermitirLoginQuandoIpNaoBloqueado() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.user.email").value("usuario@exemplo.com")) - .andExpect(jsonPath("$.data.tokens.accessToken").exists()); + .andExpect(jsonPath("$.data.tokens.access_token").exists()); } @Test @@ -324,8 +324,8 @@ void deveRenovarTokenComRefreshTokenValido() throws Exception { } """)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.tokens.accessToken").value("new-access-token")) - .andExpect(jsonPath("$.data.tokens.refreshToken").value("new-refresh-token")); + .andExpect(jsonPath("$.data.tokens.access_token").value("new-access-token")) + .andExpect(jsonPath("$.data.tokens.refresh_token").value("new-refresh-token")); } @Test @@ -388,7 +388,7 @@ void deveRetornar200QuandoLogoutForBemSucedido() throws Exception { String accessToken = jwtTokenProvider.generateToken("user@exemplo.com"); String refreshToken = jwtTokenProvider.generateRefreshToken("user@exemplo.com"); - String refreshTokenJson = "{ \"refreshToken\": \"" + refreshToken + "\" }"; + String refreshTokenJson = "{ \"refresh_token\": \"" + refreshToken + "\" }"; when(authService.logout(anyString(), anyString())).thenReturn(true); @@ -404,7 +404,7 @@ void deveRetornar200QuandoLogoutForBemSucedido() throws Exception { @DisplayName("Deve retornar 400 quando logout for chamado sem token de acesso") void deveRetornar400QuandoLogoutForChamadoSemTokenDeAcesso() throws Exception { String refreshToken = jwtTokenProvider.generateRefreshToken("user@exemplo.com"); - String refreshTokenJson = "{ \"refreshToken\": \"" + refreshToken + "\" }"; + String refreshTokenJson = "{ \"refresh_token\": \"" + refreshToken + "\" }"; mockMvc.perform(MockMvcRequestBuilders .post("/v1/auth/logout") @@ -419,7 +419,7 @@ void deveRetornar400QuandoLogoutForChamadoSemTokenDeAcesso() throws Exception { @DisplayName("Deve retornar 400 quando logout for chamado sem refresh token") void deveRetornar400QuandoLogoutForChamadoSemRefreshToken() throws Exception { String accessToken = jwtTokenProvider.generateToken("user@exemplo.com"); - String refreshTokenJson = "{ \"refreshToken\": \"\" }"; + String refreshTokenJson = "{ \"refresh_token\": \"\" }"; mockMvc.perform(MockMvcRequestBuilders .post("/v1/auth/logout") .header("Authorization", "Bearer " + accessToken) @@ -433,7 +433,7 @@ void deveRetornar400QuandoLogoutForChamadoSemRefreshToken() throws Exception { @DisplayName("Deve retornar 401 quando logout for chamado com token de acesso inválido") void deveRetornar401QuandoLogoutForChamadoComTokenDeAcessoInvalido() throws Exception { String refreshToken = jwtTokenProvider.generateRefreshToken("user@exemplo.com"); - String refreshTokenJson = "{ \"refreshToken\": \"" + refreshToken + "\" }"; + String refreshTokenJson = "{ \"refresh_token\": \"" + refreshToken + "\" }"; when(authService.logout(anyString(), anyString())) .thenThrow(new AuthenticationException("Access token inválido ou expirado")); @@ -451,7 +451,7 @@ void deveRetornar401QuandoLogoutForChamadoComTokenDeAcessoInvalido() throws Exce @DisplayName("Deve retornar 401 quando logout for chamado com refresh token inválido") void deveRetornar401QuandoLogoutForChamadoComRefreshTokenInvalido() throws Exception { String accessToken = jwtTokenProvider.generateToken("user@exemplo.com"); - String refreshTokenJson = "{ \"refreshToken\": \"invalid-refresh-token\" }"; + String refreshTokenJson = "{ \"refresh_token\": \"invalid-refresh-token\" }"; when(authService.logout(anyString(), anyString())) .thenThrow(new AuthenticationException("Refresh token inválido ou expirado")); diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/AuthServiceTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/AuthServiceTest.java index 4bda92f5..e0ddc4c6 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/AuthServiceTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/application/services/AuthServiceTest.java @@ -23,6 +23,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -206,8 +208,7 @@ void deveRenovarTokenComRefreshTokenValido() { person.setEmail("exemplo@user.com"); person.setStatus(UserStatus.ACTIVE); - when(jwtTokenProvider.isTokenExpired("valid_refresh_token")).thenReturn(false); - when(jwtTokenProvider.validateToken("valid_refresh_token")).thenReturn(true); + when(jwtTokenProvider.isTokenValid("valid_refresh_token")).thenReturn(true); when(jwtTokenProvider.getEmailFromToken("valid_refresh_token")).thenReturn("exemplo@user.com"); when(userRepository.findByEmail("exemplo@user.com")).thenReturn(person); when(jwtTokenProvider.generateToken(person.getEmail())).thenReturn("new_access_token"); @@ -239,7 +240,7 @@ void deveFalharAoRenovarTokenComRefreshTokenNuloOuVazio() { void deveFalharAoRenovarTokenComRefreshTokenInvalido() { String invalidRefreshToken = "invalid_refresh_token"; - when(jwtTokenProvider.validateToken(invalidRefreshToken)).thenReturn(false); + when(jwtTokenProvider.isTokenValid(invalidRefreshToken)).thenReturn(false); AuthenticationException ex = assertThrows( AuthenticationException.class, @@ -254,7 +255,7 @@ void deveFalharAoRenovarTokenComRefreshTokenInvalido() { void deveFalharAoRenovarTokenComRefreshTokenExpirado() { String expiredRefreshToken = "expired_refresh_token"; - when(jwtTokenProvider.isTokenExpired(expiredRefreshToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(expiredRefreshToken)).thenReturn(false); AuthenticationException ex = assertThrows( AuthenticationException.class, @@ -269,8 +270,7 @@ void deveFalharAoRenovarTokenComRefreshTokenExpirado() { void deveFalharAoRenovarTokenQuandoUsuarioNaoForEncontrado() { String validRefreshToken = "valid_refresh_token"; - when(jwtTokenProvider.isTokenExpired(validRefreshToken)).thenReturn(false); - when(jwtTokenProvider.validateToken(validRefreshToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(validRefreshToken)).thenReturn(true); when(jwtTokenProvider.getEmailFromToken(validRefreshToken)).thenReturn("exemplo@user.com"); when(userRepository.findByEmail("exemplo@user.com")).thenReturn(null); @@ -288,18 +288,18 @@ void deveFazerLogoutComTokensValidos() { String accessToken = "valid_access_token"; String refreshToken = "valid_refresh_token"; - when(jwtTokenProvider.validateToken(accessToken)).thenReturn(true); - when(jwtTokenProvider.isTokenExpired(accessToken)).thenReturn(false); - when(jwtTokenProvider.validateToken(refreshToken)).thenReturn(true); - when(jwtTokenProvider.isTokenExpired(refreshToken)).thenReturn(false); + when(jwtTokenProvider.isTokenValid(accessToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(refreshToken)).thenReturn(true); - loginService.logout(accessToken, refreshToken); + Boolean result = loginService.logout(accessToken, refreshToken); - // Verificar que o memoryBlacklistService foi injetado corretamente - assertNotNull(memoryBlacklistService); + // Verificar que o logout foi bem-sucedido + assertNotNull(result); + assertTrue(result); - // O método logout deve chamar addToBlacklist internamente - // Podemos verificar isso através do comportamento esperado + // Verificar que os tokens foram adicionados à blacklist + verify(memoryBlacklistService).addToBlacklist(accessToken); + verify(memoryBlacklistService).addToBlacklist(refreshToken); } @Test @@ -308,7 +308,7 @@ void deveFalharAoFazerLogoutComAccessTokenInvalido() { String invalidAccessToken = "invalid_access_token"; String validRefreshToken = "valid_refresh_token"; - when(jwtTokenProvider.validateToken(invalidAccessToken)).thenReturn(false); + when(jwtTokenProvider.isTokenValid(invalidAccessToken)).thenReturn(false); AuthenticationException ex = assertThrows( AuthenticationException.class, @@ -323,8 +323,8 @@ void deveFalharAoFazerLogoutComRefreshTokenInvalido() { String validAccessToken = "valid_access_token"; String invalidRefreshToken = "invalid_refresh_token"; - when(jwtTokenProvider.validateToken(validAccessToken)).thenReturn(true); - when(jwtTokenProvider.validateToken(invalidRefreshToken)).thenReturn(false); + when(jwtTokenProvider.isTokenValid(validAccessToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(invalidRefreshToken)).thenReturn(false); AuthenticationException ex = assertThrows( AuthenticationException.class, @@ -339,8 +339,7 @@ void deveFalharAoFazerLogoutComAccessTokenExpirado() { String expiredAccessToken = "expired_access_token"; String validRefreshToken = "valid_refresh_token"; - when(jwtTokenProvider.validateToken(expiredAccessToken)).thenReturn(true); - when(jwtTokenProvider.isTokenExpired(expiredAccessToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(expiredAccessToken)).thenReturn(false); AuthenticationException ex = assertThrows( AuthenticationException.class, @@ -355,10 +354,8 @@ void deveFalharAoFazerLogoutComRefreshTokenExpirado() { String validAccessToken = "valid_access_token"; String expiredRefreshToken = "expired_refresh_token"; - when(jwtTokenProvider.validateToken(validAccessToken)).thenReturn(true); - when(jwtTokenProvider.isTokenExpired(validAccessToken)).thenReturn(false); - when(jwtTokenProvider.validateToken(expiredRefreshToken)).thenReturn(true); - when(jwtTokenProvider.isTokenExpired(expiredRefreshToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(validAccessToken)).thenReturn(true); + when(jwtTokenProvider.isTokenValid(expiredRefreshToken)).thenReturn(false); AuthenticationException ex = assertThrows( AuthenticationException.class, diff --git a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java index 04ecddd2..0b62a745 100644 --- a/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java +++ b/pointtils/src/test/java/com/pointtils/pointtils/src/infrastructure/configs/JwtAuthenticationFilterTest.java @@ -167,4 +167,95 @@ void deveLimparContextoSegurancaAposProcessamento() throws ServletException, IOE SecurityContextHolder.clearContext(); assertNull(SecurityContextHolder.getContext().getAuthentication()); } + + @Test + @DisplayName("Deve permitir token blacklisted em endpoint de logout exato") + void devePermitirTokenBlacklistedEmLogoutExato() throws ServletException, IOException { + // Arrange + String token = "blacklisted_token"; + request.addHeader("Authorization", "Bearer " + token); + request.setRequestURI("/v1/auth/logout"); + when(jwtService.isTokenExpired(token)).thenReturn(false); + when(jwtService.extractClaim(eq(token), any())).thenReturn("test@email.com"); + + // Act - Não deve verificar blacklist para endpoints de logout + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Assert - Deve processar normalmente mesmo com token blacklisted + verify(filterChain).doFilter(request, response); + assertEquals(200, response.getStatus()); + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Deve permitir token blacklisted em endpoint de logout com path adicional") + void devePermitirTokenBlacklistedEmLogoutComPath() throws ServletException, IOException { + // Arrange + String token = "blacklisted_token"; + request.addHeader("Authorization", "Bearer " + token); + request.setRequestURI("/v1/auth/logout/123"); + when(jwtService.isTokenExpired(token)).thenReturn(false); + when(jwtService.extractClaim(eq(token), any())).thenReturn("test@email.com"); + + // Act + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Assert + verify(filterChain).doFilter(request, response); + assertEquals(200, response.getStatus()); + } + + @Test + @DisplayName("Deve permitir token blacklisted em endpoint de logout com substring") + void devePermitirTokenBlacklistedEmLogoutComSubstring() throws ServletException, IOException { + // Arrange + String token = "blacklisted_token"; + request.addHeader("Authorization", "Bearer " + token); + request.setRequestURI("/api/v1/auth/logout"); + when(jwtService.isTokenExpired(token)).thenReturn(false); + when(jwtService.extractClaim(eq(token), any())).thenReturn("test@email.com"); + + // Act + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Assert + verify(filterChain).doFilter(request, response); + assertEquals(200, response.getStatus()); + } + + @Test + @DisplayName("Não deve permitir token blacklisted em endpoints que não são de logout") + void naoDevePermitirTokenBlacklistedEmEndpointsNaoLogout() throws ServletException, IOException { + // Arrange + String token = "blacklisted_token"; + request.addHeader("Authorization", "Bearer " + token); + request.setRequestURI("/v1/auth/login"); + when(memoryBlacklistService.isBlacklisted(token)).thenReturn(true); + + // Act + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Assert - Deve bloquear token blacklisted em endpoints não-logout + verify(filterChain, never()).doFilter(any(), any()); + assertEquals(401, response.getStatus()); + } + + @Test + @DisplayName("Deve processar normalmente endpoint de refresh token") + void deveProcessarNormalmenteEndpointRefresh() throws ServletException, IOException { + // Arrange + String token = "valid_token"; + request.addHeader("Authorization", "Bearer " + token); + request.setRequestURI("/v1/auth/refresh"); + when(memoryBlacklistService.isBlacklisted(token)).thenReturn(false); + when(jwtService.isTokenExpired(token)).thenReturn(false); + when(jwtService.extractClaim(eq(token), any())).thenReturn("test@email.com"); + + // Act + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // Assert - Deve processar normalmente endpoint de refresh + verify(filterChain).doFilter(request, response); + assertEquals(200, response.getStatus()); + } }