package com.airlock.iam.oauth2.application.configuration.client;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.model.jwt.signature.JwsAlgorithmRsa;
import com.airlock.iam.common.domain.model.jwt.signature.JwsVerifier;
import com.airlock.iam.common.domain.service.jwt.jwks.JwksSignatureValidator;
import com.airlock.iam.common.domain.service.jwt.signature.HttpClientRemoteJWKSetLoader;
import com.airlock.iam.common.domain.service.jwt.signature.RemoteJWKSetLoader;
import com.airlock.iam.core.misc.util.httpclient.HttpClientConfig;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.oauth2.domain.model.client.OAuth2ClientSideCredentials;
import com.airlock.iam.oauth2.domain.model.client.communication.OpenIDConnectIdTokenSignatureValidator;
import com.airlock.iam.oauth2.domain.model.token.IdToken;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.license.PluginLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.LogTypes;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.time.Duration;

@Plugin(description = "OpenID Connect ID Token signature validation plugin for RS256 signatures.", displayName = "OIDC RS256 Signature Validator")
@PluginLicenseRestriction(moduleAffiliation = "OAuthClient")
public class OpenIDConnectRS256SignatureValidator implements OpenIDConnectIdTokenSignatureValidator {
   private static final ELogger LOG = ELoggerFactory.getLogger(OpenIDConnectRS256SignatureValidator.class);
   private static final Duration REMOTE_JWK_REFRESH_TIME = Duration.ofDays(7L);
   @StringProperty(
      description = "URL of the remote JWKS endpoint publishing the public keys used for ID Token RS256 signature validation. This is used for OpenID Connect providers that cycle public keys (eg. Google).<p>The remote public key should be presented according to the 'JSON Web Key' specification otherwise the parsing may fail.",
      exampleValues = {
            "https://airlock.iam/auth/rest/oauth2/authorization-servers/asId/jwks",
            "https://www.googleapis.com/oauth2/v2/certs",
            "https://www.googleapis.com/oauth2/v3/certs",
            "https://login.microsoftonline.com/common/discovery/v2.0/keys",
            "https://login.windows.net/common/discovery/keys",
            "https://login.salesforce.com/id/keys"
      },
      optional = false
   )
   private String remoteKeyLocation;
   @PluginProperty(displayName = "HTTP Client", description = "Configuration for the HTTP client used to fetch the remote public key.", optional = false)
   private HttpClientConfig httpClient;
   private RemoteJWKSetLoader keyLoader;

   public OpenIDConnectRS256SignatureValidator(String remoteKeyLocation, HttpClientConfig httpClient) {
      this.remoteKeyLocation = remoteKeyLocation;
      this.httpClient = httpClient;
   }

   @AfterConfigActivation
   void instantiateKeyLoader() throws GeneralSecurityException, IOException {
      this.keyLoader = new HttpClientRemoteJWKSetLoader(this.httpClient.createHttpClientForDomainClasses(), REMOTE_JWK_REFRESH_TIME);
   }

   @Override
   public boolean validateIdTokenSignature(IdToken idToken, OAuth2ClientSideCredentials clientCredentials) {
      return idToken.getSignedJWT().map(this::validateJwt).orElse(false);
   }

   private boolean validateJwt(JsonWebSignatureToken jwt) {
      try {
         return new JwksSignatureValidator(this.keyLoader, URI.create(this.remoteKeyLocation))
            .validateSignature(
               jwt.header().keyId(),
               key -> key.verifier()
                  .filter(v -> isSignedWithRS256Algorithm(jwt))
                  .filter(OpenIDConnectRS256SignatureValidator::containsRS256Algorithm)
                  .map(verifier -> verifier.verify(jwt))
                  .orElse(false)
            );
      } catch (FailureException e) {
         LOG.warn(LogTypes.PLUGIN_INFO, "Remote signature key update failed due to a caching problem.", e);
         return false;
      }
   }

   @VisibleForTesting
   void setKeyLoader(RemoteJWKSetLoader keyLoader) {
      this.keyLoader = keyLoader;
   }

   private static boolean containsRS256Algorithm(JwsVerifier verifier) {
      boolean containsRS256Algorithm = verifier.supportedJWSAlgorithms().contains(JwsAlgorithmRsa.RS256);
      if (!containsRS256Algorithm) {
         LOG.debug("JWT validation failed because the JWT verifier does not support the RS256 algorithm.");
      }

      return containsRS256Algorithm;
   }

   private static boolean isSignedWithRS256Algorithm(JsonWebSignatureToken jwt) {
      boolean isRS256signed = JwsAlgorithmRsa.RS256.toString().equals(jwt.header().algorithmName());
      if (!isRS256signed) {
         LOG.debug("JWT validation failed because it is not signed with the RS256 algorithm.");
      }

      return isRS256signed;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OpenIDConnectRS256SignatureValidatorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String remoteKeyLocation;
      @SuppressFBWarnings(justification = "generated code")
      private HttpClientConfig httpClient;

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

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectRS256SignatureValidator.OpenIDConnectRS256SignatureValidatorBuilder remoteKeyLocation(String remoteKeyLocation) {
         this.remoteKeyLocation = remoteKeyLocation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectRS256SignatureValidator.OpenIDConnectRS256SignatureValidatorBuilder httpClient(HttpClientConfig httpClient) {
         this.httpClient = httpClient;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectRS256SignatureValidator build() {
         return new OpenIDConnectRS256SignatureValidator(this.remoteKeyLocation, this.httpClient);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OpenIDConnectRS256SignatureValidator.OpenIDConnectRS256SignatureValidatorBuilder(remoteKeyLocation="
            + this.remoteKeyLocation
            + ", httpClient="
            + this.httpClient
            + ")";
      }
   }
}
