package com.oguerreiro.resilient.config;

/**
 * Sometimes this import disappears. I think iIt happens when solving imports (Crtl+shft+O)
 * import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
 */
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;

import java.util.function.Supplier;

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.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
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.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import com.oguerreiro.resilient.repository.security.ResilientSaml2AuthenticationRequestRepository;
import com.oguerreiro.resilient.security.AuthoritiesConstants;
import com.oguerreiro.resilient.security.saml2.Saml2AuthenticationHandler;
import com.oguerreiro.resilient.security.saml2.Saml2ResponseLoggingFilter;
import com.oguerreiro.resilient.web.filter.SpaWebFilter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tech.jhipster.config.JHipsterProperties;
import tech.jhipster.web.filter.CookieCsrfFilter;

@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {

  private final JHipsterProperties jHipsterProperties;

  private final RememberMeServices rememberMeServices;

  private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;

  public SecurityConfiguration(RememberMeServices rememberMeServices, JHipsterProperties jHipsterProperties,
      @Autowired(required = false) RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
    this.rememberMeServices = rememberMeServices;
    this.jHipsterProperties = jHipsterProperties;
    this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
    return new ResilientSaml2AuthenticationRequestRepository();
  }

  /**
   * For future knowledge in SAMLv2 config, its necessary to :
   * <ul>
   * <li>In application.yml add saml2 relyingPartyRegistration. SpringSecurity will automatically create and configure
   * the bean {@link RelyingPartyRegistrationRepository relyingPartyRegistrationRepository}, used in saml2
   * configuration.</li>
   * <li>Add in ResilientApp.main() the {@code Security.addProvider(new BouncyCastleProvider());}</li>
   * <li>Escape the path /saml2 from Angular dispatcher to index.html, in
   * SpaWebfilter.java: {@code !path.startsWith("/saml2")}</li>
   * <li>Add {@link Saml2MetadataFilter}, AFTER csrf and BEFORE other filters in
   * {@link SecurityConfiguration#filterChain()}:
   * <ul>
   * <li>Declare :
   * <ul>
   * <li>{@code OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();}</li>
   * <li>{@code Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationRepository, metadataResolver);}</li>
   * </ul>
   * 
   * </li>
   * <li>{@code .addFilterBefore(metadataFilter, BasicAuthenticationFilter.class)}</li>
   * </ul>
   * </li>
   * <li>Add /saml2 paths to permitAll() in {@link SecurityConfiguration#filterChain()}:
   * <ul>
   * <li>{@code .requestMatchers(mvc.pattern("/saml2/service-provider-metadata/**")).permitAll()}</li>
   * <li>{@code .requestMatchers(mvc.pattern("/saml2/authenticate/**")).permitAll()}</li>
   * <li>{@code .requestMatchers(mvc.pattern("/saml2/**")).permitAll()}</li>
   * </ul>
   * </li>
   * </ul>
   * 
   * <b>SecurityFilterChain</b>, if SAML2 is configured, will be appended with:
   * 
   * <pre>
   * <code>
    // SAML2 Login - Only if a relyingPartyRegistrationRepository is present
    if (this.relyingPartyRegistrationRepository != null) {
      OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();
      Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationRepository, metadataResolver);
      
      http
        .addFilterBefore(metadataFilter, BasicAuthenticationFilter.class)                          // For SAMLv2 metadata.
        .addFilterBefore(new Saml2ResponseLoggingFilter(), Saml2WebSsoAuthenticationFilter.class)  // For SAML XML Response output logging
        .saml2Login(
            saml2 -> saml2
              .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository)
              .successHandler(saml2AuthenticationHandler)
              .failureHandler(saml2AuthenticationHandler)
        )
      ;
    }
  
   * </code>
   * </pre>
   * 
   * <b>application.yml</b> will look something like this:
   * 
   * <pre>
   * <code>
  resilient:
    security:
      saml2:     # ADDED to support SAMLv2 authentication to IDP.
                 # Metadata endpoint ${base-url}/saml2/service-provider-metadata/mock-idp
      enabled: true
      base-url: https://localhost:8443
      success-url: http://localhost:4200/
      failure-url: http://localhost:4200/
        relyingparty:
          registration:
            mock-idp:
              assertingparty:
                entity-id: http://localhost:3000/saml/metadata
                single-sign-on:
                  url: http://localhost:3000/saml/sso
                single-logout:
                  url: http://localhost:3000/saml/slo
                verification:
                  credentials:
                    - certificate-location: classpath:saml/idp-public.cert
              signing:
                credentials:
                  - private-key-location: classpath:saml/private.key
                    certificate-location: classpath:saml/public.cert
   * </code>
   * </pre>
   */
  //@formatter:off
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc, Saml2AuthenticationHandler saml2AuthenticationHandler) throws Exception {   
    http
      .cors(withDefaults())
      .csrf(
        csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(
            new SpaCsrfTokenRequestHandler()))
      .addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class)
      .addFilterAfter(new CookieCsrfFilter(), BasicAuthenticationFilter.class)
      .headers(
        headers -> 
        headers
            .contentSecurityPolicy(csp -> csp.policyDirectives(jHipsterProperties.getSecurity().getContentSecurityPolicy()))
            .frameOptions(FrameOptionsConfig::sameOrigin)
            .referrerPolicy(
               referrer -> 
               referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
              )
            .permissionsPolicy(
               permissions -> 
               permissions.policy(
                          "camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()"
               )
             )
       )
      .authorizeHttpRequests(
          authz ->

          // prettier-ignore
          authz.requestMatchers(
                  mvc.pattern("/index.html"),
                  mvc.pattern("/*.js"), 
                  mvc.pattern("/*.txt"),
                  mvc.pattern("/*.json"), 
                  mvc.pattern("/*.map"),
                  mvc.pattern("/*.css")).permitAll()
                .requestMatchers(
                  mvc.pattern("/*.ico"), 
                  mvc.pattern("/*.png"),
                  mvc.pattern("/*.svg"),
                  mvc.pattern("/*.webapp")).permitAll()
                .requestMatchers(mvc.pattern("/app/**")).permitAll()
                .requestMatchers(mvc.pattern("/i18n/**")).permitAll()
                .requestMatchers(mvc.pattern("/content/**")).permitAll()
                .requestMatchers(mvc.pattern("/swagger-ui/**")).permitAll()
                .requestMatchers(mvc.pattern("/saml2/service-provider-metadata/**")).permitAll()  // ** This are specific for SAML2 authentication.
                .requestMatchers(mvc.pattern("/saml2/authenticate/**")).permitAll()               // ** If not active, probably It shouldn't be added. 
                .requestMatchers(mvc.pattern("/saml2/**")).permitAll()                            // ** I think its harmless to keep it always configured.
                .requestMatchers(mvc.pattern("/api/authenticate")).permitAll()
                /* .requestMatchers(mvc.pattern("/api/register")).permitAll()   DISABLED REQUESTS FOR NEW USERS */
                .requestMatchers(mvc.pattern("/api/activate")).permitAll()
                .requestMatchers(mvc.pattern("/api/account/reset-password/init")).permitAll()
                .requestMatchers(mvc.pattern("/api/account/reset-password/finish")).permitAll()
                .requestMatchers(mvc.pattern("/api/account/saml2-endpoint")).permitAll()
                .requestMatchers(mvc.pattern("/api/home/page")).permitAll()
                .requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN)
                .requestMatchers(mvc.pattern("/api/**")).authenticated()
                .requestMatchers(mvc.pattern("/v3/api-docs/**")).hasAuthority(AuthoritiesConstants.ADMIN)
                .requestMatchers(mvc.pattern("/management/health")).permitAll()
                .requestMatchers(mvc.pattern("/management/health/**")).permitAll()
                .requestMatchers(mvc.pattern("/management/info")).permitAll()
                /* .requestMatchers(mvc.pattern("/management/prometheus")).permitAll() //SECURED to ROLE_ADMIN */
                .requestMatchers(mvc.pattern("/management/prometheus")).hasAuthority(AuthoritiesConstants.ADMIN)
                .requestMatchers(mvc.pattern("/management/**")).hasAuthority(AuthoritiesConstants.ADMIN)
      )
      .rememberMe(
          rememberMe -> 
          rememberMe.rememberMeServices(rememberMeServices)
                    .rememberMeParameter("remember-me")
                    .key(jHipsterProperties.getSecurity().getRememberMe().getKey())
          )
      .exceptionHandling(
          exceptionHanding -> 
          exceptionHanding.defaultAuthenticationEntryPointFor(
              new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
              new OrRequestMatcher(antMatcher("/api/**"))
              )
          )
      .formLogin(
          formLogin -> 
          formLogin.loginPage("/")
                   .loginProcessingUrl("/api/authentication")
                   .successHandler(
                       (request, response, authentication) -> 
                       response.setStatus(HttpStatus.OK.value())
                    )
                   .failureHandler(
                       (request, response, exception) -> 
                       response.setStatus(HttpStatus.UNAUTHORIZED.value())
                   )
                   .permitAll()
          )
      .logout(
          logout -> 
          logout.logoutUrl("/api/logout")
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .permitAll()
          )
    ;
    
    
    // SAML2 Login - Only if a relyingPartyRegistrationRepository is present
    if (this.relyingPartyRegistrationRepository != null) {
      OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();
      Saml2MetadataFilter metadataFilter = new Saml2MetadataFilter(relyingPartyRegistrationRepository, metadataResolver);
      
      /**
       * For SAML its necessary to add in ResilientApp.main() the 
       * Security.addProvider(new BouncyCastleProvider()); 
       */
      http
        .addFilterBefore(metadataFilter, BasicAuthenticationFilter.class)                          // For SAMLv2 metadata.
        .addFilterBefore(new Saml2ResponseLoggingFilter(), Saml2WebSsoAuthenticationFilter.class)  // For SAML XML Response output logging
        .saml2Login(
            saml2 -> saml2
              .relyingPartyRegistrationRepository(relyingPartyRegistrationRepository)
              .successHandler(saml2AuthenticationHandler)
              .failureHandler(saml2AuthenticationHandler)
        )
      ;
    }
    
    return http.build();
  }
  //@formatter:on

  @Bean
  MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
    return new MvcRequestMatcher.Builder(introspector);
  }

  /**
   * Custom CSRF handler to provide BREACH protection.
   *
   * @see <a href=
   *      "https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa">Spring
   *      Security Documentation - Integrating with CSRF Protection</a>
   * @see <a href="https://github.com/jhipster/generator-jhipster/pull/25907">JHipster - use customized
   *      SpaCsrfTokenRequestHandler to handle CSRF token</a>
   * @see <a href="https://stackoverflow.com/q/74447118/65681">CSRF protection not working with Spring Security 6</a>
   */
  static final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {

    private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
      /*
       * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
       * the CsrfToken when it is rendered in the response body.
       */
      this.delegate.handle(request, response, csrfToken);
    }

    @Override
    public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
      /*
       * If the request contains a request header, use CsrfTokenRequestAttributeHandler
       * to resolve the CsrfToken. This applies when a single-page application includes
       * the header value automatically, which was obtained via a cookie containing the
       * raw CsrfToken.
       */
      if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
        return super.resolveCsrfTokenValue(request, csrfToken);
      }
      /*
       * In all other cases (e.g. if the request contains a request parameter), use
       * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
       * when a server-side rendered form includes the _csrf request parameter as a
       * hidden input.
       */
      return this.delegate.resolveCsrfTokenValue(request, csrfToken);
    }
  }
}
