package com.airlock.iam.login.app.application.configuration;

import com.airlock.iam.common.application.configuration.CorsSettings;
import com.airlock.iam.common.application.configuration.state.IamSessionSameSitePolicy;
import com.airlock.iam.common.domain.model.jwt.JwtKeys;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.service.jwt.encryption.JwtTicketDirectAesEncrypterDecrypter;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.EnumProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.PasswordPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.misc.processor.PasswordPropertyProcessor;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

@Plugin(description = "Loginapp security settings.", displayName = "Security Settings", typeShortName = "SecuritySettings")
public final class SecurityConfig {
   @StringProperty(
      description = "<p>The encryption key (encoded in base64) is required to encrypt sensitive data in cookies or in REST responses.</p><p>The <tt>openssl</tt> tool can be used to generate a random base64 string with 256 bits (32 bytes): <code>openssl rand -base64 32</code></p>",
      isSensitive = true,
      optional = false,
      displayName = "Encryption Key (Base64 Encoded)"
   )
   private String encryptionKey;
   public static final String ENCRYPTION_KEY_PROPERTY_NAME = "encryptionKey";
   public static final String ENCRYPTION_KEY_PROPERTY_DISPLAY_NAME = "Encryption Key (Base64 Encoded)";
   @StringProperty(
      description = "<p>The HMAC Key (encoded in base64) is required to sign sensitive data stored in cookies or REST responses.</p><p>The <tt>openssl</tt> tool can be used to generate a random base64 string with 512 bits (64 bytes): <code>openssl rand -base64 64</code></p><p><strong>Security Recommendation:</strong> Use an HMAC key length of 512 bit or greater for optimal security with the used HS512 algorithm.</p>",
      isSensitive = true,
      optional = false,
      displayName = "HMAC Key (Base64 Encoded)"
   )
   private String hmacKey;
   public static final String HMAC_KEY_PROPERTY_NAME = "hmacKey";
   @PluginProperty(
      description = "The settings to allow cross-domain REST calls.",
      optional = true,
      defaultValue = CorsSettings.class,
      displayName = "CORS Settings"
   )
   private CorsSettings corsSettings;
   @BooleanProperty(
      description = "<p>If enabled, REST endpoints are protected against CSRF attacks.</p><p>With this protection, the REST API only accepts requests that contain the custom header <tt>X-Same-Domain</tt> with an arbitrary non-empty value. In cross-origin resource sharing (CORS), such requests are not considered <em>simple requests</em> and thus must always be preceded by a preflight request, which prevents cross-site request forgery (CSRF) attacks.</p><p><b>Security warning:</b> Disabling this feature may allow CSRF attacks. Only do so if the REST client is unable to comply with the aforementioned restrictions.</p><p>To be RFC-compliant, the endpoint\n<tt>/&lt;loginapp-uri&gt;/rest/public/tech-client-registration/oauth2/<i>&lt;as-identifier&gt;</i>/register</tt>\nnever requires the <tt>X-Same-Domain</tt> header.</p>\n<p>Requests to this endpoint are guaranteed to be non-simple, because of the enforced non-simple content\ntype <tt>application/json</tt></p>",
      optional = true,
      displayName = "CSRF Protection",
      defaultValue = "true"
   )
   private boolean csrfProtection;
   @IntegerProperty(
      description = "<p>Defines how long it takes (in milliseconds) until IAM answers an unsuccessful request in the public part of the API.\nFaster answers are delayed until the configured duration is reached. This helps to prevent user enumeration timing attacks.\nThe configured response delay does not affect successful or slower responses.\nProtection against timing attacks is only provided if IAM is able to process 'unsuccessful' requests within the configured duration.</p>\n<p>The endpoints for the password policy check, application access check, and the user self-registration are excluded from response delaying.</p>",
      displayName = "Minimal Error Response Duration [ms]",
      exampleValues = {500, 1000, 2000},
      defaultValue = 2000,
      minValue = 0,
      optional = true
   )
   private int fixedResponseDuration;
   @RegexProperty(
      description = "Regular expression pattern used to validate usernames entered by the user.\nIf the username does not match the pattern,\nthe corresponding process is aborted and a message is logged.",
      optional = true,
      exampleValues = {"[0-9]{1,100}", "[a-zA-Z0-9]{1,100}", "[a-zA-Z0-9@._+-]{1,100}"},
      defaultValue = "[a-zA-Z0-9@._+-]{1,100}"
   )
   private Pattern usernameFilterPattern;
   @EnumProperty(
      displayName = "Session Cookie SameSite Policy",
      description = "<p>Specifies the 'SameSite' cookie attribute of the IAM session cookie 'iam-session-id'. The 'Secure' attribute is automatically set based on whether the request was performed using http or https (see exception for 'None' below).</p><ul><li>Strict: The cookie is not sent in cross-origin requests.</li><li>Lax: The cookie is sent in some cross-origin requests, such as GET requests.</li><li>None: The cookie is sent in cross-origin requests. In this case, the 'Secure' Cookie-Attribute is always set, regardless of whether the request was performed using http or https.Use this setting when using SAML2 in combination with cross-domain POST Bindings.</li><li>No SameSite Attribute: No attribute is set. Browsers apply their default behaviour, usually 'Lax'.</li></ul>",
      defaultValue = "LAX",
      optional = true
   )
   private IamSessionSameSitePolicy sameSitePolicy;

   @ConfigurationValidator(localOnly = true)
   static void validateKeyStrength(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws ConfigurationException {
      if (valEnv == Environment.CURRENT_VM || !PasswordPropertyProcessor.isExternallyStored(config, "hmacKey", env, context)) {
         String hmacKey = PasswordPropertyValueReader.effectivePasswordConfigValue(config, "hmacKey", env, context);
         if (StringUtils.isNoneBlank(new CharSequence[]{hmacKey}) && (!Base64.isBase64(hmacKey) || Base64.decodeBase64(hmacKey).length < 32)) {
            throw new PropertyConstraintViolationException(
               "hmacKey",
               config,
               env,
               context,
               "The configured value must be a base64 encoded key with at least 512 bits. See the property description on how to generate a correct key."
            );
         }
      }

      if (valEnv == Environment.CURRENT_VM || !PasswordPropertyProcessor.isExternallyStored(config, "encryptionKey", env, context)) {
         String encryptionKey = normalizeBase64(PasswordPropertyValueReader.effectivePasswordConfigValue(config, "encryptionKey", env, context));
         if (StringUtils.isNoneBlank(new CharSequence[]{encryptionKey}) && (!Base64.isBase64(encryptionKey) || Base64.decodeBase64(encryptionKey).length != 32)
            )
          {
            throw new PropertyConstraintViolationException(
               "encryptionKey",
               config,
               env,
               context,
               "The configured value must be a base64 encoded key with exactly 256 bits. See the property description on how to generate a correct key."
            );
         }
      }
   }

   @ConfigurationValidator(localOnly = true)
   static void validateEncryption(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws ConfigurationException {
      if (valEnv == Environment.CURRENT_VM) {
         String encryptionKey = normalizeBase64(StringPropertyValueReader.effectiveStringConfigValue(config, "encryptionKey", env, context));

         try {
            JwtTicketDirectAesEncrypterDecrypter encrypter = JwtTicketDirectAesEncrypterDecrypter.createDefaultEncryptorDecryptor(encryptionKey);
            encrypter.encrypt(JsonWebSignatureToken.of("eyJhbGciOiJIUzUxMiJ9.eyJjb250ZW50IjoiY29udGVudCJ9.irrelevant"));
         } catch (Exception e) {
            throw new PropertyConstraintViolationException(
               "encryptionKey", config, env, context, "Encryption not possible with the available encryption key. Maybe the key is no valid Base64 string?"
            );
         }
      }
   }

   private String getHmacKey() {
      return normalizeBase64(this.hmacKey);
   }

   private String getEncryptionKey() {
      return normalizeBase64(this.encryptionKey);
   }

   private static String normalizeBase64(String base64) {
      return Base64.encodeBase64String(Base64.decodeBase64(base64));
   }

   public JwtKeys getJwtKeys() {
      return JwtKeys.jwtKeys(this.getHmacKey(), this.getEncryptionKey());
   }

   public boolean isCsrfProtected() {
      return this.csrfProtection;
   }

   public CorsSettings getCorsSettings() {
      return this.corsSettings;
   }

   public int getFixedResponseDuration() {
      return this.fixedResponseDuration;
   }

   public Pattern getUsernameFilterPattern() {
      return this.usernameFilterPattern;
   }

   public IamSessionSameSitePolicy getSameSitePolicy() {
      return this.sameSitePolicy;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static SecurityConfig.SecurityConfigBuilder builder() {
      return new SecurityConfig.SecurityConfigBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public SecurityConfig(
      String encryptionKey,
      String hmacKey,
      CorsSettings corsSettings,
      boolean csrfProtection,
      int fixedResponseDuration,
      Pattern usernameFilterPattern,
      IamSessionSameSitePolicy sameSitePolicy
   ) {
      this.encryptionKey = encryptionKey;
      this.hmacKey = hmacKey;
      this.corsSettings = corsSettings;
      this.csrfProtection = csrfProtection;
      this.fixedResponseDuration = fixedResponseDuration;
      this.usernameFilterPattern = usernameFilterPattern;
      this.sameSitePolicy = sameSitePolicy;
   }

   @SuppressFBWarnings(justification = "generated code")
   public SecurityConfig() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class SecurityConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String encryptionKey;
      @SuppressFBWarnings(justification = "generated code")
      private String hmacKey;
      @SuppressFBWarnings(justification = "generated code")
      private CorsSettings corsSettings;
      @SuppressFBWarnings(justification = "generated code")
      private boolean csrfProtection;
      @SuppressFBWarnings(justification = "generated code")
      private int fixedResponseDuration;
      @SuppressFBWarnings(justification = "generated code")
      private Pattern usernameFilterPattern;
      @SuppressFBWarnings(justification = "generated code")
      private IamSessionSameSitePolicy sameSitePolicy;

      @SuppressFBWarnings(justification = "generated code")
      SecurityConfigBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder encryptionKey(String encryptionKey) {
         this.encryptionKey = encryptionKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder hmacKey(String hmacKey) {
         this.hmacKey = hmacKey;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder corsSettings(CorsSettings corsSettings) {
         this.corsSettings = corsSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder csrfProtection(boolean csrfProtection) {
         this.csrfProtection = csrfProtection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder fixedResponseDuration(int fixedResponseDuration) {
         this.fixedResponseDuration = fixedResponseDuration;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder usernameFilterPattern(Pattern usernameFilterPattern) {
         this.usernameFilterPattern = usernameFilterPattern;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig.SecurityConfigBuilder sameSitePolicy(IamSessionSameSitePolicy sameSitePolicy) {
         this.sameSitePolicy = sameSitePolicy;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecurityConfig build() {
         return new SecurityConfig(
            this.encryptionKey,
            this.hmacKey,
            this.corsSettings,
            this.csrfProtection,
            this.fixedResponseDuration,
            this.usernameFilterPattern,
            this.sameSitePolicy
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SecurityConfig.SecurityConfigBuilder(encryptionKey="
            + this.encryptionKey
            + ", hmacKey="
            + this.hmacKey
            + ", corsSettings="
            + this.corsSettings
            + ", csrfProtection="
            + this.csrfProtection
            + ", fixedResponseDuration="
            + this.fixedResponseDuration
            + ", usernameFilterPattern="
            + this.usernameFilterPattern
            + ", sameSitePolicy="
            + this.sameSitePolicy
            + ")";
      }
   }
}
