From d18394c898f307746d65aa2464cb006cfccbce1b Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 27 Jan 2025 14:50:35 -0300 Subject: [PATCH] feat: enhance token generation to include expiration date --- docker-compose-dev.yml | 30 +- .../dtos/authentication/TokenJwtDTO.java | 4 +- .../services/authentication/TokenService.java | 61 ++-- .../services/users/UserService.java | 5 +- .../controllers/UsersControllerTest.java | 282 +++++++++--------- 5 files changed, 190 insertions(+), 192 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index f78f96b..09b4752 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -12,7 +12,7 @@ services: - mysql-volume:/var/lib/mysql - ./database:/docker-entrypoint-initdb.d networks: - - app-network + - qualitylab-net quality-lab-pro: restart: always @@ -37,22 +37,26 @@ services: EMAIL_TO_SEND_LIST: ${EMAIL_TO_SEND_LIST} command: [ "java", "-jar", "-Dspring.profiles.active=local", "app.jar" ] networks: - - app-network + - qualitylab-net - nginx: - restart: always - image: nginx:latest - volumes: - - ./nginx/dev:/etc/nginx:ro - ports: - - '80:80' - - '443:443' - networks: - - app-network + # nginx: + # restart: always + # image: nginx:latest + # volumes: + # - ./nginx/dev:/etc/nginx:ro + # ports: + # - '80:80' + # - '443:443' + # networks: + # - qualitylab-net volumes: mysql-volume: networks: - app-network: + qualitylab-net: + name: qualitylab-net driver: bridge + attachable: true + driver_opts: + com.docker.network.bridge.name: qualitylab-net \ No newline at end of file diff --git a/src/main/java/leonardo/labutilities/qualitylabpro/dtos/authentication/TokenJwtDTO.java b/src/main/java/leonardo/labutilities/qualitylabpro/dtos/authentication/TokenJwtDTO.java index f4e727f..7a7c33f 100644 --- a/src/main/java/leonardo/labutilities/qualitylabpro/dtos/authentication/TokenJwtDTO.java +++ b/src/main/java/leonardo/labutilities/qualitylabpro/dtos/authentication/TokenJwtDTO.java @@ -1,4 +1,6 @@ package leonardo.labutilities.qualitylabpro.dtos.authentication; -public record TokenJwtDTO(String tokenJWT) { +import java.time.Instant; + +public record TokenJwtDTO(String tokenJWT, Instant dateExp) { } diff --git a/src/main/java/leonardo/labutilities/qualitylabpro/services/authentication/TokenService.java b/src/main/java/leonardo/labutilities/qualitylabpro/services/authentication/TokenService.java index 06c0218..1a91f74 100644 --- a/src/main/java/leonardo/labutilities/qualitylabpro/services/authentication/TokenService.java +++ b/src/main/java/leonardo/labutilities/qualitylabpro/services/authentication/TokenService.java @@ -4,44 +4,45 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTCreationException; import com.auth0.jwt.exceptions.JWTVerificationException; +import leonardo.labutilities.qualitylabpro.dtos.authentication.TokenJwtDTO; import leonardo.labutilities.qualitylabpro.entities.User; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.ZoneId; @Service public class TokenService { - @Value("${api.security.token.secret}") - private String SECRET; - - @Value("${api.security.issuer}") - private String ISSUER; - - public String generateToken(User user) { - try { - var algorithm = Algorithm.HMAC256(SECRET); - return JWT.create().withIssuer(ISSUER).withSubject(user.getEmail()) - .withExpiresAt(dateExp()).sign(algorithm); - } catch (JWTCreationException exception) { - throw new RuntimeException("Error generating token", exception); - } - } - - public String getSubject(String tokenJWT) { - try { - var algorithm = Algorithm.HMAC256(SECRET); - return JWT.require(algorithm).withIssuer(ISSUER).build().verify(tokenJWT).getSubject(); - } catch (JWTVerificationException exception) { - throw new JWTVerificationException("Invalid token: " + exception.getMessage(), - exception); - } - } - - private Instant dateExp() { - return LocalDateTime.now().plusHours(1).toInstant(ZoneOffset.of("-03:00")); - } + @Value("${api.security.token.secret}") + private String SECRET; + + @Value("${api.security.issuer}") + private String ISSUER; + + public TokenJwtDTO generateToken(User user) { + try { + var algorithm = Algorithm.HMAC256(SECRET); + return new TokenJwtDTO(JWT.create().withIssuer(ISSUER).withSubject(user.getEmail()) + .withExpiresAt(dateExp()).sign(algorithm), dateExp()); + } catch (JWTCreationException exception) { + throw new RuntimeException("Error generating token", exception); + } + } + + public String getSubject(String tokenJWT) { + try { + var algorithm = Algorithm.HMAC256(SECRET); + return JWT.require(algorithm).withIssuer(ISSUER).build().verify(tokenJWT).getSubject(); + } catch (JWTVerificationException exception) { + throw new JWTVerificationException("Invalid token: " + exception.getMessage(), + exception); + } + } + + private Instant dateExp() { + return LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault()).toInstant(); + } } diff --git a/src/main/java/leonardo/labutilities/qualitylabpro/services/users/UserService.java b/src/main/java/leonardo/labutilities/qualitylabpro/services/users/UserService.java index d6a51fc..d560840 100644 --- a/src/main/java/leonardo/labutilities/qualitylabpro/services/users/UserService.java +++ b/src/main/java/leonardo/labutilities/qualitylabpro/services/users/UserService.java @@ -37,8 +37,7 @@ private void sendRecoveryEmail(RecoveryEmailDTO recoveryEmailDTO) { + "\nYour Team", recoveryEmailDTO.temporaryPassword()); log.info("Sending recovery email to: {}", recoveryEmailDTO.email()); - emailService - .sendPlainTextEmail(new EmailDTO(recoveryEmailDTO.email(), subject, message)); + emailService.sendPlainTextEmail(new EmailDTO(recoveryEmailDTO.email(), subject, message)); } public void recoverPassword(String username, String email) { @@ -85,7 +84,7 @@ public TokenJwtDTO signIn(String email, String password) { emailService.notifyFailedUserLogin(user.getUsername(), user.getEmail(), LocalDateTime.now()); } - return new TokenJwtDTO(tokenService.generateToken(user)); + return tokenService.generateToken(user); } public void updateUserPassword(String name, String email, String password, String newPassword) { diff --git a/src/test/java/leonardo/labutilities/qualitylabpro/controllers/UsersControllerTest.java b/src/test/java/leonardo/labutilities/qualitylabpro/controllers/UsersControllerTest.java index d344add..8a3d551 100644 --- a/src/test/java/leonardo/labutilities/qualitylabpro/controllers/UsersControllerTest.java +++ b/src/test/java/leonardo/labutilities/qualitylabpro/controllers/UsersControllerTest.java @@ -32,12 +32,16 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; - +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; @WebMvcTest(UsersController.class) @Import(TestSecurityConfig.class) @@ -46,157 +50,145 @@ @ActiveProfiles("test") class UsersControllerTest { - @Autowired - private MockMvc mockMvc; + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private UserService userService; + + @MockitoBean + private AuthenticationManager authenticationManager; + + @MockitoBean + private TokenService tokenService; + + @MockitoBean + private UserRepository userRepository; + + @Autowired + private JacksonTester usersRecordJacksonTester; + + @Autowired + private JacksonTester loginRecordJacksonTester; + + @Autowired + private JacksonTester updatePasswordRecordJacksonTester; + + @Autowired + private JacksonTester recoverPasswordRecordJacksonTester; + + @Test + @DisplayName("Should return 204 when signing up a new user") + void signUp_return_204() throws Exception { + UsersDTO usersDTO = new UsersDTO("testUser", "Marmota2024@", "test@example.com"); + User user = new User("testUser", "password", "test@example.com", UserRoles.USER); + + when(userService.signUp(anyString(), anyString(), anyString())).thenReturn(user); + + mockMvc.perform(post("/users/sign-up").contentType(MediaType.APPLICATION_JSON) + .content(usersRecordJacksonTester.write(usersDTO).getJson())) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$.username").value("testUser")) + .andExpect(jsonPath("$.email").value("test@example.com")); + + verify(userService).signUp(usersDTO.username(), usersDTO.password(), usersDTO.email()); + } + + @Test + @DisplayName("Should return 200 and call userService.signIn when signing in") + void signIn_shouldReturn200AndCallUserService() throws Exception { + Instant dateExp = LocalDateTime.now().plusHours(1).toInstant(ZoneOffset.of("-00:00")); + // Arrange + LoginUserDTO loginRecord = new LoginUserDTO("test@example.com", "password"); + TokenJwtDTO tokenJwtDTO = new TokenJwtDTO("TokenJwt", dateExp); + + when(userService.signIn(loginRecord.email(), loginRecord.password())) + .thenReturn(tokenJwtDTO); + + mockMvc.perform(post("/users/sign-in").contentType(MediaType.APPLICATION_JSON) + .content(loginRecordJacksonTester.write(loginRecord).getJson())) + .andExpect(status().isOk()).andExpect(jsonPath("$.tokenJWT").value("TokenJwt")); + + verify(userService).signIn("test@example.com", "password"); + + } + + @Test + @DisplayName("Should return 204 when requesting password recovery") + void forgotPassword_return_204() throws Exception { + UsersDTO usersDTO = new UsersDTO("testUser", "Mandrake2024@", "test@example.com"); + + mockMvc.perform( + post("/users/password/forgot-password").contentType(MediaType.APPLICATION_JSON) + .content(usersRecordJacksonTester.write(usersDTO).getJson())) + .andExpect(status().isNoContent()); + + verify(userService).recoverPassword(usersDTO.username(), usersDTO.email()); + } + + @Test + @DisplayName("Should return 204 when changing password with recovery token") + void changePassword_return_204() throws Exception { + RecoverPasswordDTO recoverRecord = + new RecoverPasswordDTO("test@example.com", "tempPassword", "newPassword"); + + mockMvc.perform(patch("/users/password/recover").contentType(MediaType.APPLICATION_JSON) + .content(recoverPasswordRecordJacksonTester.write(recoverRecord).getJson())) + .andExpect(status().isNoContent()); - @MockitoBean - private UserService userService; + verify(userService).changePassword(recoverRecord.email(), recoverRecord.temporaryPassword(), + recoverRecord.newPassword()); + } - @MockitoBean - private AuthenticationManager authenticationManager; + @Test + @DisplayName("Should return 204 when updating password for authenticated user") + @WithMockUser + void updatePassword_return_204() throws Exception { + User user = new User("testUser", "oldPassword", "test@example.com", UserRoles.USER); - @MockitoBean - private TokenService tokenService; + Authentication authentication = + new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); - @MockitoBean - private UserRepository userRepository; + SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(authentication); - @Autowired - private JacksonTester usersRecordJacksonTester; + SecurityContextHolder.setContext(securityContext); + final var auth = (User) authentication.getPrincipal(); - @Autowired - private JacksonTester loginRecordJacksonTester; + UpdatePasswordDTO updateRecord = new UpdatePasswordDTO(auth.getUsername(), auth.getEmail(), + "oldPassword", "newPassword"); - @Autowired - private JacksonTester updatePasswordRecordJacksonTester; + mockMvc.perform(patch("/users/password").contentType(MediaType.APPLICATION_JSON) + .content(updatePasswordRecordJacksonTester.write(updateRecord).getJson()) + .with(SecurityMockMvcRequestPostProcessors.user(user))) + .andExpect(status().isNoContent()); - @Autowired - private JacksonTester recoverPasswordRecordJacksonTester; + verify(userService).updateUserPassword(updateRecord.username(), updateRecord.email(), + updateRecord.oldPassword(), updateRecord.newPassword()); + } - @Test - @DisplayName("Should return 204 when signing up a new user") - void signUp_return_204() throws Exception { - UsersDTO usersDTO = new UsersDTO("testUser", "Marmota2024@", "test@example.com"); - User user = new User("testUser", "password", "test@example.com", UserRoles.USER); + @Test + @DisplayName("Should return 400 when signing up with invalid data") + void signUp_with_invalid_data_return_400() throws Exception { + UsersDTO invalidRecord = new UsersDTO("", "", "invalid-email"); + + mockMvc.perform(post("/users/sign-up").contentType(MediaType.APPLICATION_JSON) + .content(usersRecordJacksonTester.write(invalidRecord).getJson())) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should return 401 when signing in with invalid credentials") + void signIn_with_invalid_credentials_return_401() throws Exception { + LoginUserDTO loginRecord = new LoginUserDTO("test@example.com", "wrongpassword"); - when(userService.signUp(anyString(), anyString(), anyString())).thenReturn(user); - - mockMvc.perform(post("/users/sign-up") - .contentType(MediaType.APPLICATION_JSON) - .content(usersRecordJacksonTester.write(usersDTO).getJson())) - .andExpect(status().isNoContent()) - .andExpect(jsonPath("$.username").value("testUser")) - .andExpect(jsonPath("$.email").value("test@example.com")); - - verify(userService).signUp(usersDTO.username(), usersDTO.password(), usersDTO.email()); - } - - @Test - @DisplayName("Should return 200 and call userService.signIn when signing in") - void signIn_shouldReturn200AndCallUserService() throws Exception { - // Arrange - LoginUserDTO loginRecord = new LoginUserDTO("test@example.com", "password"); - TokenJwtDTO tokenJwtDTO = new TokenJwtDTO("TokenJwt"); - - when(userService.signIn(loginRecord.email(), loginRecord.password())) - .thenReturn(tokenJwtDTO); - - mockMvc.perform(post("/users/sign-in") - .contentType(MediaType.APPLICATION_JSON) - .content(loginRecordJacksonTester - .write(loginRecord) - .getJson())).andExpect(status().isOk()) - .andExpect(jsonPath("$.tokenJWT").value("TokenJwt")); - - verify(userService).signIn("test@example.com", "password"); - - } - - @Test - @DisplayName("Should return 204 when requesting password recovery") - void forgotPassword_return_204() throws Exception { - UsersDTO usersDTO = new UsersDTO("testUser", "Mandrake2024@", "test@example.com"); - - mockMvc.perform(post("/users/password/forgot-password") - .contentType(MediaType.APPLICATION_JSON) - .content(usersRecordJacksonTester.write(usersDTO).getJson())) - .andExpect(status().isNoContent()); - - verify(userService).recoverPassword(usersDTO.username(), usersDTO.email()); - } - - @Test - @DisplayName("Should return 204 when changing password with recovery token") - void changePassword_return_204() throws Exception { - RecoverPasswordDTO recoverRecord = new RecoverPasswordDTO( - "test@example.com", "tempPassword", "newPassword"); - - mockMvc.perform(patch("/users/password/recover") - .contentType(MediaType.APPLICATION_JSON) - .content(recoverPasswordRecordJacksonTester.write(recoverRecord).getJson())) - .andExpect(status().isNoContent()); - - verify(userService).changePassword( - recoverRecord.email(), - recoverRecord.temporaryPassword(), - recoverRecord.newPassword()); - } - - @Test - @DisplayName("Should return 204 when updating password for authenticated user") - @WithMockUser - void updatePassword_return_204() throws Exception { - User user = new User("testUser", "oldPassword", "test@example.com", UserRoles.USER); - - Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); - - SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.getAuthentication()).thenReturn(authentication); - - SecurityContextHolder.setContext(securityContext); - final var auth = (User) authentication.getPrincipal(); - - UpdatePasswordDTO updateRecord = new UpdatePasswordDTO - (auth.getUsername(), auth.getEmail(), "oldPassword", "newPassword"); - - mockMvc.perform(patch("/users/password") - .contentType(MediaType.APPLICATION_JSON) - .content(updatePasswordRecordJacksonTester.write(updateRecord).getJson()) - .with(SecurityMockMvcRequestPostProcessors.user(user))) - .andExpect(status().isNoContent()); - - verify(userService).updateUserPassword( - updateRecord.username(), - updateRecord.email(), - updateRecord.oldPassword(), - updateRecord.newPassword() - ); - } - - @Test - @DisplayName("Should return 400 when signing up with invalid data") - void signUp_with_invalid_data_return_400() throws Exception { - UsersDTO invalidRecord = new UsersDTO("", "", "invalid-email"); - - mockMvc.perform(post("/users/sign-up") - .contentType(MediaType.APPLICATION_JSON) - .content(usersRecordJacksonTester.write(invalidRecord).getJson())) - .andExpect(status().isBadRequest()); - } - - @Test - @DisplayName("Should return 401 when signing in with invalid credentials") - void signIn_with_invalid_credentials_return_401() throws Exception { - LoginUserDTO loginRecord = new LoginUserDTO("test@example.com", "wrongpassword"); - - when(userService.signIn(any(), any())) - .thenThrow(new BadCredentialsException("Authentication failed at")); - - mockMvc.perform(post("/users/sign-in") - .contentType(MediaType.APPLICATION_JSON) - .content(loginRecordJacksonTester.write(loginRecord).getJson())) - .andExpect(status().isUnauthorized()); - - verify(userService, times(1)).signIn(loginRecord.email(), loginRecord.password()); - } -} \ No newline at end of file + when(userService.signIn(any(), any())) + .thenThrow(new BadCredentialsException("Authentication failed at")); + + mockMvc.perform(post("/users/sign-in").contentType(MediaType.APPLICATION_JSON) + .content(loginRecordJacksonTester.write(loginRecord).getJson())) + .andExpect(status().isUnauthorized()); + + verify(userService, times(1)).signIn(loginRecord.email(), loginRecord.password()); + } +}