|
1 | 1 | package cv.igrp.platform.access_management.shared.security; |
2 | 2 |
|
| 3 | +import jakarta.servlet.FilterChain; |
| 4 | +import jakarta.servlet.ServletException; |
| 5 | +import jakarta.servlet.http.HttpServletRequest; |
| 6 | +import jakarta.servlet.http.HttpServletResponse; |
| 7 | +import org.slf4j.Logger; |
| 8 | +import org.slf4j.LoggerFactory; |
| 9 | +import org.springframework.beans.factory.annotation.Value; |
3 | 10 | import org.springframework.context.annotation.Bean; |
4 | 11 | import org.springframework.context.annotation.Configuration; |
5 | 12 | import org.springframework.context.annotation.Profile; |
| 13 | +import org.springframework.core.annotation.Order; |
| 14 | +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
6 | 15 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
7 | 16 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
8 | 17 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
9 | 18 | import org.springframework.security.config.http.SessionCreationPolicy; |
| 19 | +import org.springframework.security.core.authority.SimpleGrantedAuthority; |
| 20 | +import org.springframework.security.core.context.SecurityContextHolder; |
| 21 | +import org.springframework.security.core.userdetails.User; |
10 | 22 | import org.springframework.security.web.SecurityFilterChain; |
| 23 | +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
| 24 | +import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; |
11 | 25 | import org.springframework.web.cors.CorsConfiguration; |
| 26 | +import org.springframework.web.filter.OncePerRequestFilter; |
| 27 | + |
| 28 | +import java.io.IOException; |
| 29 | +import java.util.Collections; |
12 | 30 |
|
13 | 31 | import static org.springframework.security.config.Customizer.withDefaults; |
14 | 32 |
|
|
17 | 35 | @Profile("basic-auth") // Only active if 'basic-auth' profile is enabled |
18 | 36 | public class BasicAuthSecurityConfiguration { |
19 | 37 |
|
| 38 | + private static final Logger log = LoggerFactory.getLogger(BasicAuthSecurityConfiguration.class); |
| 39 | + |
| 40 | + @Value("${igrp.access.m2m.sync-token:}") |
| 41 | + private String machineAuthToken; |
| 42 | + |
| 43 | + /** |
| 44 | + * Filter that authenticates M2M requests using a static token header. |
| 45 | + */ |
| 46 | + private class MachineAuthFilter extends OncePerRequestFilter { |
| 47 | + @Override |
| 48 | + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| 49 | + throws ServletException, IOException { |
| 50 | + |
| 51 | + if (!request.getRequestURI().startsWith("/api/m2m/")) { |
| 52 | + filterChain.doFilter(request, response); |
| 53 | + return; |
| 54 | + } |
| 55 | + |
| 56 | + String client = request.getHeader("X-Machine-Service-ID"); |
| 57 | + String header = request.getHeader("X-Machine-Auth-Token"); |
| 58 | + |
| 59 | + if (header == null || !header.equals(machineAuthToken)) { |
| 60 | + log.warn("[M2M] Unauthorized access: missing or invalid authentication"); |
| 61 | + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| 62 | + response.getWriter().write("Unauthorized: Invalid or missing machine-to-machine authentication token."); |
| 63 | + return; |
| 64 | + } |
| 65 | + |
| 66 | + var authority = new SimpleGrantedAuthority("ROLE_M2M"); |
| 67 | + var principal = new User( |
| 68 | + (client != null && !client.isBlank()) ? client : "m2m-client", |
| 69 | + "N/A", |
| 70 | + Collections.singletonList(authority) |
| 71 | + ); |
| 72 | + var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities()); |
| 73 | + |
| 74 | + var context = SecurityContextHolder.createEmptyContext(); |
| 75 | + context.setAuthentication(authentication); |
| 76 | + SecurityContextHolder.setContext(context); |
| 77 | + |
| 78 | + // Persist the context |
| 79 | + new RequestAttributeSecurityContextRepository().saveContext(context, request, response); |
| 80 | + |
| 81 | + log.info("[M2M] Authenticated machine client: {}", principal.getUsername()); |
| 82 | + |
| 83 | + filterChain.doFilter(request, response); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + // --- Security chain 1: machine-to-machine endpoints --- |
| 88 | + @Bean |
| 89 | + @Order(1) |
| 90 | + public SecurityFilterChain m2mSecurityFilterChain(HttpSecurity http) throws Exception { |
| 91 | + var securityContextRepository = new RequestAttributeSecurityContextRepository(); |
| 92 | + |
| 93 | + http.securityMatcher("/api/m2m/**") |
| 94 | + .csrf(AbstractHttpConfigurer::disable) |
| 95 | + .securityContext(ctx -> ctx |
| 96 | + .securityContextRepository(securityContextRepository) |
| 97 | + ) |
| 98 | + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) |
| 99 | + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) |
| 100 | + .addFilterBefore(new BasicAuthSecurityConfiguration.MachineAuthFilter(), UsernamePasswordAuthenticationFilter.class); |
| 101 | + |
| 102 | + return http.build(); |
| 103 | + } |
| 104 | + |
| 105 | + // --- Security chain 2: Basic Auth for other endpoints --- |
20 | 106 | @Bean |
| 107 | + @Order(2) |
21 | 108 | public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception { |
22 | 109 |
|
23 | 110 | http.csrf(AbstractHttpConfigurer::disable); |
24 | 111 |
|
25 | 112 | http.authorizeHttpRequests(auth -> auth |
26 | 113 | .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", |
27 | | - "/swagger-resources/**", "/webjars/**", "/actuator/**").permitAll() |
| 114 | + "/swagger-resources/**", "/webjars/**", "/actuator/**", "/api/m2m/**").permitAll() |
28 | 115 | .anyRequest().authenticated() |
29 | 116 | ) |
30 | 117 | .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) |
|
0 commit comments