WebSecurity.java

package se.jobtechdev.personaldatagateway.api.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
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.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import se.jobtechdev.personaldatagateway.api.security.RequestHeaderAuthenticationProvider;
import se.jobtechdev.personaldatagateway.api.security.RequestMethodPattern;

import java.util.Collections;
import java.util.function.Predicate;

@Configuration
@EnableWebSecurity
public class WebSecurity {
  private static final Logger logger = LoggerFactory.getLogger(WebSecurity.class);

  private final RequestHeaderAuthenticationProvider requestHeaderAuthenticationProvider;

  private final SecurityConfig securityConfig;

  @Autowired
  public WebSecurity(
      RequestHeaderAuthenticationProvider requestHeaderAuthenticationProvider,
      SecurityConfig securityConfig) {
    this.requestHeaderAuthenticationProvider = requestHeaderAuthenticationProvider;
    this.securityConfig = securityConfig;
  }

  public static Predicate<RequestMethodPattern> isRolePopulated() {
    return methodPattern -> !methodPattern.roles().isEmpty();
  }

  public Customizer<
          AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>
      getAuthorizeHttpRequestsCustomizer() {
    return registry -> {
      final var methodPatterns = securityConfig.getMethodPatterns();
      for (final var methodPattern : methodPatterns) {
        final var method = methodPattern.method();
        final var pattern = methodPattern.pattern();
        final var roles = methodPattern.roles();

        if (!roles.isEmpty()) {
          registry.requestMatchers(method, pattern).hasAnyAuthority(roles.toArray(String[]::new));
        } else {
          registry.requestMatchers(method, pattern).permitAll();
        }
      }
    };
  }

  public static AuthenticationEntryPoint getAuthenticationEntryPoint() {
    return (request, response, exception) -> {
      logger.info("Authentication failed", exception);
      response.setStatus(HttpStatus.UNAUTHORIZED.value());
    };
  }

  public static Customizer<ExceptionHandlingConfigurer<HttpSecurity>> getExceptionHandler(
      AuthenticationEntryPoint authenticationEntryPoint) {
    return exceptions -> exceptions.authenticationEntryPoint(authenticationEntryPoint);
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http.cors(Customizer.withDefaults())
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(getSessionManagementConfigurerCustomizer())
        .addFilterBefore(requestHeaderAuthenticationFilter(), HeaderWriterFilter.class)
        .authorizeHttpRequests(getAuthorizeHttpRequestsCustomizer())
        .exceptionHandling(getExceptionHandler(getAuthenticationEntryPoint()))
        .build();
  }

  public Customizer<SessionManagementConfigurer<HttpSecurity>>
      getSessionManagementConfigurerCustomizer() {
    return session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  }

  @Bean
  public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() {
    final var filter = new RequestHeaderAuthenticationFilter();
    filter.setPrincipalRequestHeader("X-Auth-Key");

    final var protectedRequestMatchers =
        securityConfig.getMethodPatterns().stream()
            .filter(isRolePopulated())
            .map(
                methodPattern ->
                    new AntPathRequestMatcher(methodPattern.pattern(), methodPattern.method()))
            .toArray(AntPathRequestMatcher[]::new);

    filter.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(protectedRequestMatchers));
    filter.setAuthenticationManager(authenticationManager());

    return filter;
  }

  @Bean
  protected AuthenticationManager authenticationManager() {
    return new ProviderManager(Collections.singletonList(requestHeaderAuthenticationProvider));
  }
}