diff --git a/pom.xml b/pom.xml index c65ec1ae..0774bb25 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ mandacaru-broker https://sonarcloud.io 2.0.2 + 6.2.2 @@ -43,6 +44,22 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.security + spring-security-core + ${spring.security.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + org.flywaydb flyway-core @@ -76,12 +93,6 @@ java-jwt 4.4.0 - - org.springframework.security - spring-security-core - 5.0.7.RELEASE - - org.junit.jupiter @@ -99,7 +110,7 @@ checkstyle 10.13.0 - + diff --git a/src/main/java/com/mandacarubroker/config/ApplicationConfig.java b/src/main/java/com/mandacarubroker/config/ApplicationConfig.java new file mode 100644 index 00000000..efa208a3 --- /dev/null +++ b/src/main/java/com/mandacarubroker/config/ApplicationConfig.java @@ -0,0 +1,24 @@ +package com.mandacarubroker.config; + +import com.mandacarubroker.service.PasswordHashingService; +import com.mandacarubroker.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; + +@Configuration +@RequiredArgsConstructor +public class ApplicationConfig { + private final UserService userService; + private final PasswordHashingService passwordHashingService; + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userService); + authProvider.setPasswordEncoder(passwordHashingService); + return authProvider; + } +} diff --git a/src/main/java/com/mandacarubroker/domain/user/User.java b/src/main/java/com/mandacarubroker/domain/user/User.java index e08e4f05..45ad39db 100644 --- a/src/main/java/com/mandacarubroker/domain/user/User.java +++ b/src/main/java/com/mandacarubroker/domain/user/User.java @@ -9,8 +9,11 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; import java.time.LocalDate; +import java.util.Collection; @Table(name = "users") @Entity(name = "users") @@ -18,8 +21,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of = "id") -public class User { - +public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; @@ -40,4 +42,32 @@ public User(final RequestUserDTO requestUserDTO) { this.birthDate = requestUserDTO.birthDate(); this.balance = requestUserDTO.balance(); } + + public Collection getAuthorities() { + return null; + } + + public String getPassword() { + return password; + } + + public String getUsername() { + return username; + } + + public boolean isAccountNonExpired() { + return true; + } + + public boolean isAccountNonLocked() { + return true; + } + + public boolean isCredentialsNonExpired() { + return true; + } + + public boolean isEnabled() { + return true; + } } diff --git a/src/main/java/com/mandacarubroker/security/JwtAuthFilter.java b/src/main/java/com/mandacarubroker/security/JwtAuthFilter.java new file mode 100644 index 00000000..b92a657a --- /dev/null +++ b/src/main/java/com/mandacarubroker/security/JwtAuthFilter.java @@ -0,0 +1,58 @@ +package com.mandacarubroker.security; + +import com.mandacarubroker.service.TokenService; +import com.mandacarubroker.service.UserService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + private final TokenService tokenService; + private final UserService userService; + + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + final String token = authHeader.substring(7); + final String userId = tokenService.getTokenSubject(token); + + if (userId == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + filterChain.doFilter(request, response); + return; + } + + UserDetails user = userService.loadUserByUsername(userId); + + if (user == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + filterChain.doFilter(request, response); + return; + } + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user,null, user.getAuthorities()); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/mandacarubroker/security/SecurityConfiguration.java b/src/main/java/com/mandacarubroker/security/SecurityConfiguration.java new file mode 100644 index 00000000..5a032a22 --- /dev/null +++ b/src/main/java/com/mandacarubroker/security/SecurityConfiguration.java @@ -0,0 +1,43 @@ +package com.mandacarubroker.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity +public class SecurityConfiguration { + private static final String[] WHITE_LIST_URL = { + "/", + "/auth/login", + "/auth/register", + }; + + private final AuthenticationProvider authenticationProvider; + private final JwtAuthFilter jwtAuthFilter; + + @Bean + public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) + .authorizeHttpRequests(req -> { + req.requestMatchers(WHITE_LIST_URL).permitAll(); + req.anyRequest().authenticated(); + }) + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/java/com/mandacarubroker/service/AuthService.java b/src/main/java/com/mandacarubroker/service/AuthService.java index 0fed7d12..daefe76a 100644 --- a/src/main/java/com/mandacarubroker/service/AuthService.java +++ b/src/main/java/com/mandacarubroker/service/AuthService.java @@ -30,7 +30,7 @@ public Optional login(final RequestAuthUserDTO requestAuthU return Optional.empty(); } - String userId = user.get().getId(); + String userId = user.get().getUsername(); ResponseAuthUserDTO responseAuthUserDTO = tokenService.encodeToken(userId); return Optional.of(responseAuthUserDTO); } diff --git a/src/main/java/com/mandacarubroker/service/PasswordHashingService.java b/src/main/java/com/mandacarubroker/service/PasswordHashingService.java index 437e1273..3c360123 100644 --- a/src/main/java/com/mandacarubroker/service/PasswordHashingService.java +++ b/src/main/java/com/mandacarubroker/service/PasswordHashingService.java @@ -1,17 +1,18 @@ package com.mandacarubroker.service; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service -public class PasswordHashingService { +public class PasswordHashingService implements PasswordEncoder { private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); - public String hashPassword(final String plainPassword) { + public String encode(final CharSequence plainPassword) { return passwordEncoder.encode(plainPassword); } - public boolean matches(final String plainPassword, final String hashedPassword) { - return passwordEncoder.matches(plainPassword, hashedPassword); + public boolean matches(final CharSequence rawPassword, final String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); } } diff --git a/src/main/java/com/mandacarubroker/service/TokenService.java b/src/main/java/com/mandacarubroker/service/TokenService.java index af0370ba..ee70332c 100644 --- a/src/main/java/com/mandacarubroker/service/TokenService.java +++ b/src/main/java/com/mandacarubroker/service/TokenService.java @@ -46,6 +46,11 @@ private ResponseAuthUserDTO tryToEncodeToken(final String subject) { public String getTokenSubject(final String token) { DecodedJWT decodedToken = decodeUserToken(token); + + if (decodedToken == null) { + return null; + } + return decodedToken.getSubject(); } diff --git a/src/main/java/com/mandacarubroker/service/UserService.java b/src/main/java/com/mandacarubroker/service/UserService.java index 3ebbc229..848ca5d9 100644 --- a/src/main/java/com/mandacarubroker/service/UserService.java +++ b/src/main/java/com/mandacarubroker/service/UserService.java @@ -4,6 +4,9 @@ import com.mandacarubroker.domain.user.ResponseUserDTO; import com.mandacarubroker.domain.user.User; import com.mandacarubroker.domain.user.UserRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -13,7 +16,7 @@ import static com.mandacarubroker.validation.RecordValidation.validateRequestDTO; @Service -public class UserService { +public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordHashingService passwordHashingService = new PasswordHashingService(); @@ -26,8 +29,7 @@ private ResponseUserDTO userToResponseUserDTO(final User user) { user.getFirstName(), user.getLastName(), user.getBirthDate(), - user.getBalance() - ); + user.getBalance()); } public List getAllUsers() { @@ -50,7 +52,7 @@ public Optional getUserById(final String userId) { private User hashPassword(final User user) { final String rawPassword = user.getPassword(); - final String hashedPassword = passwordHashingService.hashPassword(rawPassword); + final String hashedPassword = passwordHashingService.encode(rawPassword); user.setPassword(hashedPassword); return user; } @@ -66,7 +68,7 @@ public Optional updateUser(final String userId, final RequestUs validateRequestDTO(requestUserDTO); final String rawPassword = requestUserDTO.password(); - final String hashedPassword = passwordHashingService.hashPassword(rawPassword); + final String hashedPassword = passwordHashingService.encode(rawPassword); return userRepository.findById(userId) .map(user -> { @@ -85,6 +87,21 @@ public Optional updateUser(final String userId, final RequestUs public void deleteUser(final String id) { userRepository.deleteById(id); } + + public UserDetails loadUserByUsername(final String username) { + if (username == null || username.isEmpty()) { + throw new UsernameNotFoundException("Username is required"); + } + + User user = userRepository.findByUsername(username); + + if (user == null) { + throw new UsernameNotFoundException("User not found"); + } + + return user; + } + public boolean verifyDuplicateUsername(final String userName) { User alreadyExistingUser = userRepository.findByUsername(userName); return alreadyExistingUser != null; diff --git a/src/test/java/com/mandacarubroker/controller/AuthControllerTest.java b/src/test/java/com/mandacarubroker/controller/AuthControllerTest.java index 60107fb5..5555ceee 100644 --- a/src/test/java/com/mandacarubroker/controller/AuthControllerTest.java +++ b/src/test/java/com/mandacarubroker/controller/AuthControllerTest.java @@ -32,7 +32,7 @@ class AuthControllerTest { private final String invalidUsername = "invalidUsername"; private final String validPassword = "password"; private final String invalidPassword = "invalidPassword"; - private final String validHashedPassword = passwordHashingService.hashPassword(validPassword); + private final String validHashedPassword = passwordHashingService.encode(validPassword); private final String validFirstName = "Lara"; private final String validLastName = "Souza"; private final LocalDate validBirthDate = LocalDate.of(1997,4,5); diff --git a/src/test/java/com/mandacarubroker/controller/StockControllerIT.java b/src/test/java/com/mandacarubroker/controller/StockControllerIT.java index 19120898..564168ea 100644 --- a/src/test/java/com/mandacarubroker/controller/StockControllerIT.java +++ b/src/test/java/com/mandacarubroker/controller/StockControllerIT.java @@ -27,7 +27,7 @@ import java.util.Optional; @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) class StockControllerIT { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/mandacarubroker/controller/UserControllerIT.java b/src/test/java/com/mandacarubroker/controller/UserControllerIT.java index 75a6fe19..028c5b45 100644 --- a/src/test/java/com/mandacarubroker/controller/UserControllerIT.java +++ b/src/test/java/com/mandacarubroker/controller/UserControllerIT.java @@ -1,6 +1,7 @@ package com.mandacarubroker.controller; import com.fasterxml.jackson.databind.ObjectMapper; + import com.mandacarubroker.domain.user.RequestUserDTO; import com.mandacarubroker.domain.user.ResponseUserDTO; import com.mandacarubroker.domain.user.User; @@ -26,11 +27,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @SpringBootTest -@AutoConfigureMockMvc +@AutoConfigureMockMvc(addFilters = false) class UserControllerIT { - @Autowired private UserRepository userRepository; @@ -48,7 +47,7 @@ class UserControllerIT { private final String validPassword = "#pass555"; private final String validFirstName = "Lara"; private final String validLastName = "Souza"; - private final LocalDate validBirthDate = LocalDate.of(2006,2,28); + private final LocalDate validBirthDate = LocalDate.of(2006, 2, 28); private final double validBalance = 90.50; private final RequestUserDTO invalidEmailUserDTO = new RequestUserDTO( "marcosloiola@.yahoo.com", @@ -57,8 +56,7 @@ class UserControllerIT { "Marcos", "Loiola", LocalDate.of(2002, 2, 26), - 0.25 - ); + 0.25); private final RequestUserDTO invalidPasswordUserDTO = new RequestUserDTO( "marcosloiola@yahoo.com", "Marcos23", @@ -66,8 +64,7 @@ class UserControllerIT { "Marcos", "Loiola", LocalDate.of(2002, 2, 26), - 0.25 - ); + 0.25); private final RequestUserDTO invalidAgeUserDTO = new RequestUserDTO( "marcosloiola@yahoo.com", "Marcos23", @@ -75,8 +72,7 @@ class UserControllerIT { "Marcos", "Loiola", LocalDate.of(2006, 3, 2), - 0.25 - ); + 0.25); private final RequestUserDTO invalidBalanceUserDTO = new RequestUserDTO( "marcosloiola@yahoo.com", @@ -85,8 +81,7 @@ class UserControllerIT { "Marcos", "Loiola", LocalDate.of(2001, 3, 2), - -0.001 - ); + -0.001); private final RequestUserDTO validUserDTO = new RequestUserDTO( validEmail, @@ -95,8 +90,7 @@ class UserControllerIT { validFirstName, validLastName, validBirthDate, - validBalance - ); + validBalance); private final String urlRequestInvalidUser = "/users/dummy-user-id"; private User user; @@ -113,19 +107,18 @@ void setUp() { user.getFirstName(), user.getLastName(), user.getBirthDate(), - user.getBalance() - ); + user.getBalance()); } @AfterEach void tearDown() { User alreadyExistentUser = userRepository.findByUsername(validUsername); - if(alreadyExistentUser != null){ + if (alreadyExistentUser != null) { service.deleteUser(alreadyExistentUser.getId()); } } - void assertResponseUserDTO(final RequestUserDTO userRequestDTO, final ResponseUserDTO receivedUser){ + void assertResponseUserDTO(final RequestUserDTO userRequestDTO, final ResponseUserDTO receivedUser) { assertEquals(userRequestDTO.firstName(), receivedUser.firstName()); assertEquals(userRequestDTO.lastName(), receivedUser.lastName()); assertEquals(userRequestDTO.birthDate(), receivedUser.birthDate()); diff --git a/src/test/java/com/mandacarubroker/service/AuthServiceTest.java b/src/test/java/com/mandacarubroker/service/AuthServiceTest.java index 5bf2b7c8..a76a7865 100644 --- a/src/test/java/com/mandacarubroker/service/AuthServiceTest.java +++ b/src/test/java/com/mandacarubroker/service/AuthServiceTest.java @@ -70,7 +70,7 @@ void setUp() { Mockito.when(passwordHashingService.matches(invalidPassword, validHashedPassword)).thenReturn(false); tokenService = Mockito.mock(TokenService.class); - Mockito.when(tokenService.encodeToken(validUser.getId())).thenReturn(new ResponseAuthUserDTO(validToken, expiresIn, tokenType)); + Mockito.when(tokenService.encodeToken(validUser.getUsername())).thenReturn(new ResponseAuthUserDTO(validToken, expiresIn, tokenType)); authService = new AuthService(userRepository, passwordHashingService, tokenService); } diff --git a/src/test/java/com/mandacarubroker/service/PasswordHashingServiceTest.java b/src/test/java/com/mandacarubroker/service/PasswordHashingServiceTest.java index bf7efc33..29371ddb 100644 --- a/src/test/java/com/mandacarubroker/service/PasswordHashingServiceTest.java +++ b/src/test/java/com/mandacarubroker/service/PasswordHashingServiceTest.java @@ -9,7 +9,7 @@ class PasswordHashingServiceTest { @Test void itShouldBeAbleToHashAndMatchRightPassword() { final PasswordHashingService underTest = new PasswordHashingService(); - final String hashedPassword = underTest.hashPassword("password"); + final String hashedPassword = underTest.encode("password"); final boolean matches = underTest.matches("password", hashedPassword); assertTrue(matches); } @@ -17,8 +17,8 @@ void itShouldBeAbleToHashAndMatchRightPassword() { @Test void itShouldNotMatchWrongPassword() { final PasswordHashingService underTest = new PasswordHashingService(); - final String hashedPassword = underTest.hashPassword("password"); + final String hashedPassword = underTest.encode("password"); final boolean matches = underTest.matches("wrongPassword", hashedPassword); assertFalse(matches); } -} \ No newline at end of file +}