From 831c97956fbc9488c273e479045a83fdd384364c Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 17 Dec 2025 17:52:14 -0300 Subject: [PATCH 1/2] Add security configs and a feign client for the User MS --- build.gradle | 7 +++ .../taskscheduler/business/dto/UserDTO.java | 15 +++++ .../infrastructure/client/UserClient.java | 14 +++++ .../security/JwtRequestFilter.java | 60 +++++++++++++++++++ .../infrastructure/security/JwtUtil.java | 45 ++++++++++++++ .../security/SecurityConfig.java | 53 ++++++++++++++++ .../security/UserDetailsServiceImpl.java | 23 +++++++ src/main/resources/application.properties | 1 + 8 files changed, 218 insertions(+) create mode 100644 src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java create mode 100644 src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java create mode 100644 src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java create mode 100644 src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java create mode 100644 src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java create mode 100644 src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java diff --git a/build.gradle b/build.gradle index 8589ac2..1177756 100644 --- a/build.gradle +++ b/build.gradle @@ -33,12 +33,19 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'io.jsonwebtoken:jjwt-api:0.13.0' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-data-mongodb-test' testImplementation 'org.springframework.boot:spring-boot-starter-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.13.0' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.13.0' } dependencyManagement { diff --git a/src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java b/src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java new file mode 100644 index 0000000..11b71fe --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/business/dto/UserDTO.java @@ -0,0 +1,15 @@ +package com.javanauta.taskscheduler.business.dto; + +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserDTO { + private String email; + private String password; +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java new file mode 100644 index 0000000..26fa406 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java @@ -0,0 +1,14 @@ +package com.javanauta.taskscheduler.infrastructure.client; + +import com.javanauta.taskscheduler.business.dto.UserDTO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "user", url = "${user.url}") +public interface UserClient { + + @GetMapping + UserDTO getUserByEmail(@RequestParam("email") String email, @RequestHeader("Authorization") String token); +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java new file mode 100644 index 0000000..13f665f --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtRequestFilter.java @@ -0,0 +1,60 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +// Defines the JwtRequestFilter class, which extends OncePerRequestFilter +public class JwtRequestFilter extends OncePerRequestFilter { + + // Defines properties to store instances of JwtUtil and UserDetailsService + private final JwtUtil jwtUtil; + private final UserDetailsServiceImpl userDetailsService; + + // Constructor that initializes the properties with provided instances + public JwtRequestFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + // Method called once per request to process the filter + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + // Gets the value of the "Authorization" header from the request + final String authorizationHeader = request.getHeader("Authorization"); + + // Checks whether the header exists and starts with "Bearer " + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + // Extracts the JWT token from the header + final String token = authorizationHeader.substring(7); + // Extracts the username from the JWT token + final String username = jwtUtil.extractUsername(token); + + // If the username is not null and the user is not yet authenticated + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // Loads user details from the username + UserDetails userDetails = userDetailsService.loadUserDetails(username, token); + // Validates the JWT token + if (jwtUtil.validateToken(token, username)) { + // Creates an authentication object with user information + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + // Sets authentication in the security context + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + } + + // Continues the filter chain, allowing the request to proceed + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java new file mode 100644 index 0000000..530ba22 --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/JwtUtil.java @@ -0,0 +1,45 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Service +public class JwtUtil { + + // Secret key used to sign and verify JWT tokens + private final String secretKey = "your-super-long-secret-key-that-needs-to-be-very-difficult"; + + // Extracts claims from the JWT token + public Claims extractClaims(String token) { + return Jwts.parser() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) // Sets secret key to validate signature + .build() + .parseClaimsJws(token) // Parses the JWT token and gets claims + .getBody(); // Returns claims body + } + + // Extracts username from the JWT token + public String extractUsername(String token) { + // Gets the subject (username) from the claims + return extractClaims(token).getSubject(); + } + + // Checks whether the JWT token is expired + public boolean isTokenExpired(String token) { + // Compares token expiration date with current date + return extractClaims(token).getExpiration().before(new Date()); + } + + // Validates the JWT token by checking username match and expiration + public boolean validateToken(String token, String username) { + // Extracts username from the token + final String extractedUsername = extractUsername(token); + // Checks if username matches and token is not expired + return (extractedUsername.equals(username) && !isTokenExpired(token)); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java new file mode 100644 index 0000000..f85ca7d --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + // JwtUtil and UserDetailsService instances injected by Spring + private final JwtUtil jwtUtil; + private final UserDetailsServiceImpl userDetailsService; + + // Constructor for dependency injection of JwtUtil and UserDetailsService + @Autowired + public SecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + // Security filter configuration + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // Creates a JwtRequestFilter instance using JwtUtil and UserDetailsService + JwtRequestFilter jwtRequestFilter = new JwtRequestFilter(jwtUtil, userDetailsService); + + http + .csrf(AbstractHttpConfigurer::disable) // Disables CSRF protection for REST APIs (not needed in stateless APIs) + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() // Requires authentication for all other requests + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Sets session policy to stateless + ) + .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // Adds JWT filter before default authentication filter + + // Returns the built security configuration + return http.build(); + } +} diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..dc1c16c --- /dev/null +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/security/UserDetailsServiceImpl.java @@ -0,0 +1,23 @@ +package com.javanauta.taskscheduler.infrastructure.security; + +import com.javanauta.taskscheduler.business.dto.UserDTO; +import com.javanauta.taskscheduler.infrastructure.client.UserClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailsServiceImpl { + + @Autowired + private UserClient userClient; + + public UserDetails loadUserDetails(String email, String token) { + UserDTO userDTO = userClient.getUserByEmail(email, token); + + return org.springframework.security.core.userdetails.User + .withUsername(userDTO.getEmail()) // Sets the username as the email + .password(userDTO.getPassword()) // Sets the user's password + .build(); // Builds the UserDetails object + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bbc2dd4..2ad97a1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=task-scheduler +user.url=localhost:8080 From 5986931c22dda7d57110306405939bae578a570b Mon Sep 17 00:00:00 2001 From: Enzo Bom Date: Wed, 17 Dec 2025 18:02:26 -0300 Subject: [PATCH 2/2] Fix based on PR review --- .../taskscheduler/infrastructure/client/UserClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java b/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java index 26fa406..7fa06d0 100644 --- a/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java +++ b/src/main/java/com/javanauta/taskscheduler/infrastructure/client/UserClient.java @@ -9,6 +9,6 @@ @FeignClient(name = "user", url = "${user.url}") public interface UserClient { - @GetMapping + @GetMapping("/user") UserDTO getUserByEmail(@RequestParam("email") String email, @RequestHeader("Authorization") String token); }