355 lines
16 KiB
Java
355 lines
16 KiB
Java
package de.avatic.lcc.config;
|
|
|
|
import de.avatic.lcc.model.db.users.User;
|
|
import de.avatic.lcc.repositories.users.GroupRepository;
|
|
import de.avatic.lcc.repositories.users.UserRepository;
|
|
import de.avatic.lcc.service.apps.JwtTokenService;
|
|
import io.jsonwebtoken.Claims;
|
|
import jakarta.servlet.FilterChain;
|
|
import jakarta.servlet.ServletException;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.context.annotation.Bean;
|
|
import org.springframework.context.annotation.Configuration;
|
|
import org.springframework.context.annotation.Profile;
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
import org.springframework.security.core.GrantedAuthority;
|
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
|
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
|
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
|
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
|
import org.springframework.security.web.SecurityFilterChain;
|
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
|
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
|
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
|
import org.springframework.security.web.csrf.CsrfToken;
|
|
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
|
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.web.cors.CorsConfiguration;
|
|
import org.springframework.web.cors.CorsConfigurationSource;
|
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.function.Supplier;
|
|
|
|
|
|
@Configuration
|
|
@EnableMethodSecurity
|
|
public class SecurityConfig {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
|
|
@Value("${lcc.allowed_cors}")
|
|
private String allowedCors;
|
|
|
|
@Value("${lcc.allowed_oauth_token_cors:*}") // Default: alle Origins
|
|
private String oauthTokenCors;
|
|
|
|
@Bean
|
|
@Profile("!dev & !test") // Only active when NOT in dev profile
|
|
public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http, JwtTokenService jwtTokenService) throws Exception {
|
|
http
|
|
.cors(cors -> cors.configurationSource(prodCorsConfigurationSource())) // Production CORS
|
|
.authorizeHttpRequests(auth -> auth
|
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
|
.requestMatchers("/oauth2/token").permitAll()
|
|
.requestMatchers("/api/**").authenticated()
|
|
.requestMatchers("/api/dev/**").denyAll()
|
|
.anyRequest().authenticated()
|
|
)
|
|
.oauth2Login(oauth2 -> oauth2
|
|
.defaultSuccessUrl("/", true)
|
|
)
|
|
.oauth2ResourceServer(oauth2 -> oauth2
|
|
.bearerTokenResolver(bearerTokenResolver(jwtTokenService))
|
|
.jwt(jwt -> jwt
|
|
.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
|
)
|
|
)
|
|
.exceptionHandling(ex -> ex
|
|
.defaultAuthenticationEntryPointFor(
|
|
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
|
new AntPathRequestMatcher("/api/**")
|
|
)
|
|
)
|
|
.csrf(csrf -> csrf
|
|
.ignoringRequestMatchers("/oauth2/token")
|
|
.ignoringRequestMatchers("/login/oauth2/code/**")
|
|
.requireCsrfProtectionMatcher(request -> {
|
|
String requestUri = request.getRequestURI();
|
|
if (requestUri.startsWith("/oauth2/") || requestUri.startsWith("/login/oauth2/")) {
|
|
return false;
|
|
}
|
|
|
|
String authHeader = request.getHeader("Authorization");
|
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
return false;
|
|
}
|
|
|
|
return !"GET".equalsIgnoreCase(request.getMethod());
|
|
})
|
|
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
|
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
|
|
)
|
|
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
|
|
.addFilterBefore(
|
|
new SelfIssuedJwtFilter(jwtTokenService),
|
|
BearerTokenAuthenticationFilter.class
|
|
);
|
|
|
|
return http.build();
|
|
}
|
|
|
|
@Bean
|
|
@Profile("!dev & !test")
|
|
public CorsConfigurationSource prodCorsConfigurationSource() {
|
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
|
|
CorsConfiguration tokenConfiguration = new CorsConfiguration();
|
|
if ("*".equals(oauthTokenCors)) {
|
|
tokenConfiguration.setAllowedOriginPatterns(List.of("*"));
|
|
} else {
|
|
String[] tokenOrigins = oauthTokenCors.split(",");
|
|
for (int i = 0; i < tokenOrigins.length; i++) {
|
|
tokenOrigins[i] = tokenOrigins[i].trim();
|
|
}
|
|
if (tokenOrigins.length != 0) {
|
|
tokenConfiguration.setAllowedOrigins(Arrays.asList(tokenOrigins));
|
|
}
|
|
}
|
|
tokenConfiguration.setAllowedMethods(Arrays.asList("POST", "OPTIONS"));
|
|
tokenConfiguration.setAllowedHeaders(List.of("*"));
|
|
tokenConfiguration.setAllowCredentials(true);
|
|
tokenConfiguration.setMaxAge(3600L);
|
|
|
|
|
|
source.registerCorsConfiguration("/oauth2/token", tokenConfiguration);
|
|
|
|
|
|
CorsConfiguration configuration = new CorsConfiguration();
|
|
if ("*".equals(allowedCors)) {
|
|
configuration.setAllowedOriginPatterns(List.of("*"));
|
|
} else {
|
|
String[] origins = allowedCors.split(",");
|
|
for (int i = 0; i < origins.length; i++) {
|
|
origins[i] = origins[i].trim();
|
|
}
|
|
if (origins.length != 0) {
|
|
configuration.setAllowedOrigins(Arrays.asList(origins));
|
|
}
|
|
}
|
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
|
configuration.setAllowedHeaders(List.of("*"));
|
|
configuration.setAllowCredentials(true);
|
|
configuration.setMaxAge(3600L);
|
|
configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page"));
|
|
|
|
source.registerCorsConfiguration("/**", configuration);
|
|
|
|
return source;
|
|
}
|
|
|
|
@Bean
|
|
@Profile("dev | test")
|
|
public SecurityFilterChain devSecurityFilterChain(HttpSecurity http, UserRepository userRepository, JwtTokenService jwtTokenService) throws Exception {
|
|
return http
|
|
.cors(cors -> cors.configurationSource(devCorsConfigurationSource())) // Dev CORS
|
|
.authorizeHttpRequests(auth -> auth
|
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
|
.requestMatchers("/oauth2/token").permitAll()
|
|
.requestMatchers("/api/**").permitAll()
|
|
.requestMatchers("/api/dev/**").permitAll()
|
|
.anyRequest().permitAll())
|
|
.csrf(csrf -> csrf
|
|
.ignoringRequestMatchers("/oauth2/token") // CSRF für OAuth deaktivieren
|
|
.ignoringRequestMatchers("/api/**") // Optional: für alle API-Endpunkte
|
|
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
|
|
.csrfTokenRequestHandler(new LccCsrfTokenRequestHandler())
|
|
)
|
|
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
|
|
.addFilterBefore(new DevUserEmulationFilter(userRepository), BasicAuthenticationFilter.class)
|
|
.addFilterBefore(
|
|
new SelfIssuedJwtFilter(jwtTokenService),
|
|
BearerTokenAuthenticationFilter.class
|
|
)
|
|
.build();
|
|
}
|
|
|
|
@Bean
|
|
@Profile("dev | test")
|
|
public CorsConfigurationSource devCorsConfigurationSource() {
|
|
CorsConfiguration configuration = new CorsConfiguration();
|
|
configuration.setAllowedOriginPatterns(List.of("http://localhost:*"));
|
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
|
configuration.setAllowedHeaders(List.of("*"));
|
|
configuration.setAllowCredentials(true);
|
|
configuration.setMaxAge(3600L);
|
|
|
|
configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Page-Count", "X-Current-Page"));
|
|
|
|
|
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
source.registerCorsConfiguration("/**", configuration);
|
|
return source;
|
|
}
|
|
|
|
@Bean
|
|
public JwtAuthenticationConverter jwtAuthenticationConverter() {
|
|
// Für Entra ID Tokens
|
|
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
|
|
converter.setAuthoritiesClaimName("scp");
|
|
converter.setAuthorityPrefix("SCOPE_");
|
|
|
|
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
|
|
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
|
|
return jwtConverter;
|
|
}
|
|
|
|
@Bean
|
|
public PasswordEncoder passwordEncoder() {
|
|
return new BCryptPasswordEncoder();
|
|
}
|
|
|
|
|
|
@Bean
|
|
@Profile("!dev & !test")
|
|
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(UserRepository userRepository, GroupRepository groupRepository) {
|
|
final OidcUserService delegate = new OidcUserService();
|
|
|
|
return (userRequest) -> {
|
|
OidcUser oidcUser = delegate.loadUser(userRequest);
|
|
Integer userId = null;
|
|
|
|
// Debug: Print all claims
|
|
// System.out.println("=== ID Token Claims ===");
|
|
// oidcUser.getIdToken().getClaims().forEach((key, value) ->
|
|
// System.out.println(key + ": " + value)
|
|
// );
|
|
// System.out.println("======================");
|
|
|
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(oidcUser.getAuthorities());
|
|
|
|
User user = null;
|
|
|
|
String workdayId = oidcUser.getAttribute("workday_id");
|
|
|
|
// Try different ways to get email
|
|
String email = oidcUser.getEmail();
|
|
if (email == null) {
|
|
email = oidcUser.getAttribute("email");
|
|
}
|
|
if (email == null) {
|
|
email = oidcUser.getAttribute("upn");
|
|
}
|
|
if (email == null) {
|
|
email = oidcUser.getAttribute("preferred_username");
|
|
}
|
|
|
|
|
|
if (workdayId != null) {
|
|
user = userRepository.getByWorkdayId(workdayId);
|
|
if (user != null) {
|
|
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName())));
|
|
userId = user.getId();
|
|
}
|
|
} else if (email != null) {
|
|
user = userRepository.getByEmail(email);
|
|
if (user != null) {
|
|
user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName().toUpperCase())));
|
|
userId = user.getId();
|
|
}
|
|
}
|
|
|
|
if (user == null) {
|
|
var isFirstUser = userRepository.count() == 0;
|
|
userRepository.update(LccOidcUser.createDatabaseUser(email, oidcUser.getGivenName(), oidcUser.getFamilyName(), workdayId, isFirstUser));
|
|
mappedAuthorities.add(new SimpleGrantedAuthority(isFirstUser ? "ROLE_SERVICE" : "ROLE_NONE"));
|
|
}
|
|
|
|
|
|
return new LccOidcUser(
|
|
mappedAuthorities,
|
|
oidcUser.getIdToken(),
|
|
oidcUser.getUserInfo(),
|
|
"preferred_username",
|
|
userId
|
|
);
|
|
};
|
|
|
|
|
|
}
|
|
|
|
@Bean
|
|
@Profile("!dev & !test")
|
|
public BearerTokenResolver bearerTokenResolver(JwtTokenService jwtTokenService) {
|
|
return request -> {
|
|
String authHeader = request.getHeader("Authorization");
|
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
String token = authHeader.substring(7);
|
|
|
|
try {
|
|
Claims claims = jwtTokenService.parseClaimsWithoutValidation(token);
|
|
String tokenType = claims.get("token_type", String.class);
|
|
if ("ext_app".equals(tokenType)) {
|
|
return null; // using the SelfIssuedJwtFilter
|
|
}
|
|
} catch (Exception e) {
|
|
// carry on ...
|
|
}
|
|
|
|
return token; // some other token
|
|
}
|
|
return null; // all other requests
|
|
};
|
|
}
|
|
|
|
public static final class LccCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
|
|
private final CsrfTokenRequestHandler delegate = new CsrfTokenRequestAttributeHandler();
|
|
|
|
@Override
|
|
public void handle(HttpServletRequest request, HttpServletResponse response,
|
|
Supplier<CsrfToken> csrfToken) {
|
|
this.delegate.handle(request, response, csrfToken);
|
|
}
|
|
|
|
@Override
|
|
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
|
|
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
|
|
return super.resolveCsrfTokenValue(request, csrfToken);
|
|
}
|
|
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
|
|
}
|
|
}
|
|
|
|
public static final class CsrfCookieFilter extends OncePerRequestFilter {
|
|
@Override
|
|
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response,
|
|
@NotNull FilterChain filterChain) throws ServletException, IOException {
|
|
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
|
if (csrfToken != null) {
|
|
csrfToken.getToken();
|
|
}
|
|
filterChain.doFilter(request, response);
|
|
}
|
|
}
|
|
}
|