package eu.bitfield.recipes.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import static org.springframework.http.HttpMethod.*; @Configuration @EnableWebFluxSecurity public class SecurityConfiguration { private SecurityWebFilterChain filterChainDefaults(ServerHttpSecurity http) { // disable session management // https://github.com/spring-projects/spring-security/issues/6552#issuecomment-519398510 return http.csrf(ServerHttpSecurity.CsrfSpec::disable) .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) .build(); } @Order(Ordered.HIGHEST_PRECEDENCE) @Bean public SecurityWebFilterChain accountEndpointFilterChain(ServerHttpSecurity httpSecurity) { httpSecurity.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/account/**")) .authorizeExchange(exchange -> exchange.anyExchange().permitAll()); return filterChainDefaults(httpSecurity); } @Order(Ordered.HIGHEST_PRECEDENCE) @Bean public SecurityWebFilterChain recipeEndpointFilterChain(ServerHttpSecurity httpSecurity) { httpSecurity.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/recipe/**")) .httpBasic(Customizer.withDefaults()) .authorizeExchange(exchange -> { exchange.pathMatchers(GET, "/api/recipe/{recipeId:\\d+}").permitAll() .pathMatchers(GET, "/api/recipe/search").permitAll() .anyExchange().authenticated(); }); return filterChainDefaults(httpSecurity); } @Bean public SecurityWebFilterChain fallbackFilterChain(ServerHttpSecurity httpSecurity) { httpSecurity.authorizeExchange(exchange -> exchange.anyExchange().denyAll()); return filterChainDefaults(httpSecurity); } @Bean public PasswordEncoder passwordEncoder() { // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id // accessed at 2025-04-22 int saltLength = 16; int hashLength = 32; int parallelism = 1; int memory = 1 << 16; // in KiB = 64 MiB int iterations = 2; return new Argon2PasswordEncoder(saltLength, hashLength, parallelism, memory, iterations); } }