From 5f681a7a1bd11a246675db4bf4839e4c85f98cd7 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 19 May 2024 22:12:44 +0200 Subject: [PATCH] - Added JwtAuthenticationFilter - Added UploadHistory - Added secure endpoints to SecurityConfig --- .../sharepulse/config/SecurityConfig.java | 31 +++++--- .../rest/mappings/UploadHistory.java | 20 +++++ .../security/JwtAuthenticationFilter.java | 74 +++++++++++++++++++ .../service/AuthenticationService.java | 6 +- 4 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/w665/sharepulse/rest/mappings/UploadHistory.java create mode 100644 src/main/java/de/w665/sharepulse/rest/security/JwtAuthenticationFilter.java diff --git a/src/main/java/de/w665/sharepulse/config/SecurityConfig.java b/src/main/java/de/w665/sharepulse/config/SecurityConfig.java index 75999f4..1bf48e9 100644 --- a/src/main/java/de/w665/sharepulse/config/SecurityConfig.java +++ b/src/main/java/de/w665/sharepulse/config/SecurityConfig.java @@ -1,32 +1,45 @@ package de.w665.sharepulse.config; +import de.w665.sharepulse.rest.security.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; 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.LogoutConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http + .csrf(csrf -> csrf.ignoringRequestMatchers("/api/v1/**")) // Disable CSRF for API routes + .sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security + ) .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/api/v1/secure/**").authenticated() - .anyRequest().permitAll() - ) - .formLogin(formLogin -> formLogin - .loginPage("/management/login") - .permitAll() + .requestMatchers("/api/v1/secure/**").authenticated() // Secure these endpoints + .anyRequest().permitAll() // All other requests are allowed without authentication ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Apply JWT filter .logout(LogoutConfigurer::permitAll) - .csrf(csrf -> csrf - .ignoringRequestMatchers("/api/v1/**") // Disable CSRF for /api/** - ) .rememberMe(Customizer.withDefaults()); + return http.build(); } + + } + +// TODO: Fix the security configuration to allow public access to unsecured endpoints diff --git a/src/main/java/de/w665/sharepulse/rest/mappings/UploadHistory.java b/src/main/java/de/w665/sharepulse/rest/mappings/UploadHistory.java new file mode 100644 index 0000000..1cbeecf --- /dev/null +++ b/src/main/java/de/w665/sharepulse/rest/mappings/UploadHistory.java @@ -0,0 +1,20 @@ +package de.w665.sharepulse.rest.mappings; + +import de.w665.sharepulse.rest.SecureApiRestController; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class UploadHistory extends SecureApiRestController { + + @GetMapping("/test") + public ResponseEntity test(HttpServletRequest request) { + log.debug("Received test request"); + return ResponseEntity.ok("Test successful"); + } +} diff --git a/src/main/java/de/w665/sharepulse/rest/security/JwtAuthenticationFilter.java b/src/main/java/de/w665/sharepulse/rest/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..57d312f --- /dev/null +++ b/src/main/java/de/w665/sharepulse/rest/security/JwtAuthenticationFilter.java @@ -0,0 +1,74 @@ +package de.w665.sharepulse.rest.security; + +import de.w665.sharepulse.service.AuthenticationService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +@Component +@Slf4j +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final AuthenticationService authenticationService; + private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/api/v1/secure/**"); + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { + + logger.debug("Filtering request: " + request.getRequestURI()); + + if(!requestMatcher.matches(request)) { + logger.debug("Request does not match the secure path. Skipping JWT authentication."); + filterChain.doFilter(request, response); + return; + } + + try { + String jwt = getJwtFromRequest(request); + if (jwt != null && authenticationService.validateToken(jwt)) { + String username = authenticationService.extractSubject(jwt); + // Extract the role from the JWT and set it to Spring AuthenticationContext for access control + String role = authenticationService.getClaimValue(jwt, "role", String.class); + List authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, authorities); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + // SUCCESSFUL AUTHENTICATION + filterChain.doFilter(request, response); + } else { + logger.warn("Unauthorized: Authentication token is missing or invalid."); + } + } catch (Exception ex) { + logger.warn("Could not set user authentication in security context. An error occurred during JWT processing.", ex); + } + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/de/w665/sharepulse/service/AuthenticationService.java b/src/main/java/de/w665/sharepulse/service/AuthenticationService.java index 6df00ce..1ec2a62 100644 --- a/src/main/java/de/w665/sharepulse/service/AuthenticationService.java +++ b/src/main/java/de/w665/sharepulse/service/AuthenticationService.java @@ -57,9 +57,10 @@ public class AuthenticationService { Date expiryDate = new Date(nowMillis + expirationTime); return Jwts.builder() - .subject(username.getUsername()) + .subject("SharePulse Authentication Token") .issuedAt(now) .claim("role", username.getRole()) + .claim("username", username.getUsername()) .expiration(expiryDate) .signWith(secretKey) .compact(); @@ -83,8 +84,7 @@ public class AuthenticationService { * Retrieves a typed claim from the JWT. * @param token the JWT from which to extract the claim * @param claimName the name of the claim to retrieve - * @param claimType the Class object of the type T of the claim - * @param the expected type of the claim value + * @param claimType the Class object of the expected type of the claim value * @return the value of the specified claim as type T, or null if not found or in case of an error * Usage example: getClaimValue(token, "role", String.class) */