Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Frontend
Submodule Frontend deleted from 4c1860
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ public ResponseEntity<LoginResponseDTO> login(@RequestBody LoginRequestDTO login

@PostMapping("/refresh")
@Operation(summary = "Renova sessão de usuário")
public ResponseEntity<RefreshTokenResponseDTO> refreshToken(@RequestBody String token) {
RefreshTokenResponseDTO response = authService.refreshToken(token);
public ResponseEntity<RefreshTokenResponseDTO> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ public ResponseEntity<RefreshTokenResponseDTO> generateTokens(@RequestParam Stri
@PostMapping("/refresh-token")
@Operation(summary = "Gera um novo access token usando um refresh token válido")
public ResponseEntity<RefreshTokenResponseDTO> 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());
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O método getEmailFromToken() não é chamado após a validação do token em isTokenValid(). Se o token for inválido, este método pode falhar ou retornar dados incorretos. Recomendo validar se o método getEmailFromToken() trata adequadamente tokens inválidos ou mover esta chamada para dentro de um bloco try-catch.

Suggested change
String username = jwtService.getEmailFromToken(request.getRefreshToken());
String username;
try {
username = jwtService.getEmailFromToken(request.getRefreshToken());
} catch (Exception e) {
throw new com.pointtils.pointtils.src.core.domain.exceptions.AuthenticationException("Falha ao extrair usuário do refresh token: " + e.getMessage());
}

Copilot uses AI. Check for mistakes.

String newAccessToken = jwtService.generateToken(username);
String newRefreshToken = jwtService.generateRefreshToken(username);

Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand All @@ -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);
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adicionar o refresh token à blacklist pode ser problemático se o mesmo refresh token for usado para gerar novos access tokens. Considere se é necessário invalidar o refresh token imediatamente ou se deveria ser invalidado apenas quando expirar naturalmente, dependendo da estratégia de rotação de tokens do sistema.

Suggested change
memoryBlacklistService.addToBlacklist(refreshToken);
// Do not blacklist the refresh token here; let it expire naturally or handle via rotation if needed.

Copilot uses AI. Check for mistakes.


return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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")
Comment on lines +85 to +86
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A verificação requestURI.contains("/logout") é muito permissiva e pode ser explorada por atacantes criando URIs como /malicious/logout/endpoint que passariam pela validação. Recomendo usar verificação mais específica como requestURI.matches("^/v\\d+/auth/logout/?$") ou manter apenas as comparações exatas.

Suggested change
requestURI.startsWith("/v1/auth/logout/") ||
requestURI.contains("/logout")
requestURI.startsWith("/v1/auth/logout/")

Copilot uses AI. Check for mistakes.

);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Comment on lines +105 to +118
Copy link

Copilot AI Sep 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method catches all exceptions with a generic Exception which may hide specific JWT validation issues. Consider catching specific JWT exceptions (JwtException, ExpiredJwtException, etc.) and logging the specific error for better debugging while still returning false.

Copilot uses AI. Check for mistakes.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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);

Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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"));
Expand All @@ -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"));
Expand Down
Loading