package com.sun.identity.saml2.profile;

import com.airlock.iam.opensso.jaxb.entityconfig.BaseConfigType;
import com.airlock.iam.opensso.jaxb.metadata.EndpointType;
import com.airlock.iam.opensso.jaxb.metadata.IDPSSODescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.KeyDescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.KeyTypes;
import com.airlock.iam.opensso.jaxb.metadata.RoleDescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.SPSSODescriptorType;
import com.sun.identity.plugin.session.SessionException;
import com.sun.identity.saml.common.SAMLUtils;
import com.sun.identity.saml.xmlsig.KeyProvider;
import com.sun.identity.saml2.assertion.EncryptedID;
import com.sun.identity.saml2.assertion.Issuer;
import com.sun.identity.saml2.assertion.NameID;
import com.sun.identity.saml2.common.SAML2Exception;
import com.sun.identity.saml2.common.SAML2SDKUtils;
import com.sun.identity.saml2.common.SAML2Utils;
import com.sun.identity.saml2.key.EncInfo;
import com.sun.identity.saml2.key.KeyUtil;
import com.sun.identity.saml2.logging.LogUtil;
import com.sun.identity.saml2.meta.SAML2MetaException;
import com.sun.identity.saml2.meta.SAML2MetaManager;
import com.sun.identity.saml2.meta.SAML2MetaUtils;
import com.sun.identity.saml2.protocol.Extensions;
import com.sun.identity.saml2.protocol.LogoutRequest;
import com.sun.identity.saml2.protocol.LogoutResponse;
import com.sun.identity.saml2.protocol.ProtocolFactory;
import com.sun.identity.saml2.protocol.SessionIndex;
import com.sun.identity.saml2.protocol.Status;
import com.sun.identity.saml2.protocol.StatusDetail;
import com.sun.identity.shared.encode.Base64;
import com.sun.identity.shared.encode.URLEncDec;
import com.sun.identity.shared.xml.XMLUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.xml.soap.SOAPException;
import jakarta.xml.soap.SOAPMessage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class LogoutUtil {
   private static final KeyProvider KEY_PROVIDER = KeyUtil.getKeyProviderInstance();
   private static final SAML2MetaManager META_MANAGER = SAML2Utils.getSAML2MetaManager();

   public static StringBuffer doLogout(
      String metaAlias,
      String recipientEntityID,
      List<EndpointType> recipientSLOList,
      List<String> extensionsList,
      String binding,
      String relayState,
      String sessionIndex,
      NameID nameID,
      HttpServletRequest request,
      HttpServletResponse response,
      Map<String, String> paramsMap,
      BaseConfigType config
   ) throws SAML2Exception, SessionException {
      StringBuffer logoutRequestID = new StringBuffer();
      String classMethod = "LogoutUtil.doLogout: ";
      String requesterEntityID = META_MANAGER.getEntityByMetaAlias(metaAlias);
      String realm = SAML2MetaUtils.getRealmByMetaAlias(metaAlias);
      String hostEntityRole = SAML2Utils.getHostEntityRole(paramsMap);
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG
            .message(
               classMethod
                  + "Entering ...\nrequesterEntityID="
                  + requesterEntityID
                  + "\nrecipientEntityID="
                  + recipientEntityID
                  + "\nbinding="
                  + binding
                  + "\nrelayState="
                  + relayState
                  + "\nsessionIndex="
                  + sessionIndex
            );
      }

      String requestID = SAML2Utils.generateID();
      if (requestID != null && !requestID.isEmpty()) {
         String destinationURI = SAML2Utils.getParameter(paramsMap, "Destination");
         String consent = SAML2Utils.getParameter(paramsMap, "Consent");
         Extensions extensions = createExtensions(extensionsList);
         Issuer issuer = SAML2Utils.createIssuer(requesterEntityID);
         LogoutRequest logoutReq = null;

         try {
            logoutReq = ProtocolFactory.getInstance().createLogoutRequest();
         } catch (Exception e) {
            SAML2SDKUtils.DEBUG.error(classMethod + "Unable to create LogoutRequest : ", e);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorCreatingLogoutRequest"));
         }

         logoutReq.setID(requestID);
         logoutReq.setVersion("2.0");
         logoutReq.setIssueInstant(new Date());
         setNameIDForSLORequest(logoutReq, nameID, realm, requesterEntityID, hostEntityRole, recipientEntityID);
         logoutReq.setDestination(destinationURI);
         logoutReq.setConsent(consent);
         logoutReq.setIssuer(issuer);
         if (hostEntityRole.equals("IDPRole")) {
            int effectiveTime = 600;
            String effectiveTimeStr = SAML2Utils.getAttributeValueFromSSOConfig(realm, requesterEntityID, "IDPRole", "assertionEffectiveTime");
            if (effectiveTimeStr != null) {
               try {
                  effectiveTime = Integer.parseInt(effectiveTimeStr);
                  if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                     SAML2SDKUtils.DEBUG.message(classMethod + "got effective time from config:" + effectiveTime);
                  }
               } catch (NumberFormatException nfe) {
                  SAML2SDKUtils.DEBUG.error(classMethod + "Failed to get assertion effective time from IDP SSO config: ", nfe);
                  effectiveTime = 600;
               }
            }

            Date date = new Date();
            date.setTime(date.getTime() + effectiveTime * 1000L);
            logoutReq.setNotOnOrAfter(date);
         }

         if (extensions != null) {
            logoutReq.setExtensions(extensions);
         }

         if (sessionIndex != null) {
            List<String> list = new ArrayList<>();
            list.add(sessionIndex);
            logoutReq.setSessionIndex(list);
         }

         String location = getSLOServiceLocation(recipientSLOList, binding);
         if (location != null && !location.isEmpty()) {
            SAML2SDKUtils.DEBUG.message(classMethod + "Recipient's single logout service location = " + location);
            if (destinationURI == null || destinationURI.isEmpty()) {
               logoutReq.setDestination(location);
            }

            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(classMethod + "SLO Request before signing : ");
               SAML2SDKUtils.DEBUG.message(logoutReq.toXMLString(true, true));
            }

            if (binding.equals("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")) {
               try {
                  doSLOByHttpRedirect(
                     logoutReq.toXMLString(true, true), location, relayState, realm, requesterEntityID, hostEntityRole, recipientEntityID, response
                  );
                  logoutRequestID.append(requestID);
                  String[] data = new String[]{location};
                  LogUtil.access(Level.INFO, "REDIRECT_TO_SP_OR_IDP", data, null);
               } catch (Exception e) {
                  SAML2SDKUtils.DEBUG.error("Exception :", e);
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorRedirectingLogoutRequest"));
               }
            } else if (binding.equals("urn:oasis:names:tc:SAML:2.0:bindings:SOAP")) {
               logoutRequestID.append(requestID);
               signSLORequest(logoutReq, realm, requesterEntityID, hostEntityRole, recipientEntityID);
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message(classMethod + "SLO Request after signing : ");
                  SAML2SDKUtils.DEBUG.message(logoutReq.toXMLString(true, true));
               }

               location = SAML2Utils.fillInBasicAuthInfo(config, location);
               doSLOBySOAP(requestID, logoutReq, location, realm, requesterEntityID, hostEntityRole, request, response);
            } else if (binding.equals("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")) {
               logoutRequestID.append(requestID);
               signSLORequest(logoutReq, realm, requesterEntityID, hostEntityRole, recipientEntityID);
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message(classMethod + "SLO Request after signing : ");
                  SAML2SDKUtils.DEBUG.message(logoutReq.toXMLString(true, true));
               }

               doSLOByPOST(logoutReq.toXMLString(true, true), location, relayState, request, response);
            }

            SPCache.logoutRequestIDHash().put(logoutRequestID.toString(), logoutReq);
            return logoutRequestID;
         } else {
            SAML2SDKUtils.DEBUG.error(classMethod + "Unable to find the recipient's single logout service with the binding " + binding);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("sloServiceNotfound"));
         }
      } else {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("cannotGenerateID"));
      }
   }

   private static void doSLOByHttpRedirect(
      String sloRequestXMLString,
      String sloURL,
      String relayState,
      String realm,
      String hostEntity,
      String hostEntityRole,
      String remoteEntity,
      HttpServletResponse response
   ) throws SAML2Exception, IOException {
      String method = "doSLOByHttpRedirect: ";
      String encodedXML = SAML2Utils.encodeForRedirect(sloRequestXMLString);
      StringBuilder queryString = new StringBuilder().append("SAMLRequest").append("=").append(encodedXML);
      if (relayState != null && !relayState.isEmpty()) {
         String tmp = SAML2Utils.generateID();
         if (hostEntityRole.equals("IDPRole")) {
            IDPCache.relayStateCache().put(tmp, relayState);
         } else {
            SPCache.relayStateHash().put(tmp, relayState);
         }

         queryString.append("&").append("RelayState").append("=").append(tmp);
      }

      boolean needToSign = false;
      if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
         needToSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, "SPRole");
      } else {
         needToSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, "IDPRole");
      }

      String signedQueryString = queryString.toString();
      if (needToSign) {
         signedQueryString = SAML2Utils.signQueryString(signedQueryString, realm, hostEntity, hostEntityRole);
      }

      String redirectURL = sloURL + (sloURL.contains("?") ? "&" : "?") + signedQueryString;
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message(method + "LogoutRequestXMLString : " + sloRequestXMLString);
         SAML2SDKUtils.DEBUG.message(method + "LogoutRedirectURL : " + sloURL);
      }

      response.sendRedirect(redirectURL);
   }

   private static void doSLOBySOAP(
      String requestID,
      LogoutRequest sloRequest,
      String sloURL,
      String realm,
      String hostEntity,
      String hostRole,
      HttpServletRequest request,
      HttpServletResponse response
   ) throws SAML2Exception, SessionException {
      String sloRequestXMLString = sloRequest.toXMLString(true, true);
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message("LogoutUtil.doSLOBySOAP : SLORequestXML: " + sloRequestXMLString + "\nSOAPURL : " + sloURL);
      }

      SOAPMessage resMsg = null;

      try {
         resMsg = SAML2Utils.sendSOAPMessage(sloRequestXMLString, sloURL, true);
      } catch (SOAPException se) {
         SAML2SDKUtils.DEBUG.error("Unable to send SOAPMessage to IDP ", se);
         throw new SAML2Exception(se.getMessage());
      }

      Element respElem = SAML2Utils.getSamlpElement(resMsg, "LogoutResponse");
      LogoutResponse sloResponse = ProtocolFactory.getInstance().createLogoutResponse(respElem);
      String userId = null;
      if (hostRole != null && hostRole.equals("SPRole")) {
         userId = SPSingleLogout.preSingleLogoutProcess(
            hostEntity, realm, request, response, null, sloRequest, sloResponse, "urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
         );
      }

      if (sloResponse == null) {
         SAML2SDKUtils.DEBUG.error("LogoutUtil.doSLOBySoap : null response");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullLogoutResponse"));
      }

      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message("LogoutUtil.doSLOBySOAP : LogoutResponse without SOAP envelope:\n" + sloResponse.toXMLString());
      }

      Issuer resIssuer = sloResponse.getIssuer();
      String requestId = sloResponse.getInResponseTo();
      SAML2Utils.verifyResponseIssuer(realm, hostEntity, resIssuer, requestId);
      String remoteEntityID = sloResponse.getIssuer().getValue();
      verifySLOResponse(sloResponse, realm, remoteEntityID, hostEntity, hostRole);
      boolean success = checkSLOResponse(sloResponse, requestID);
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message("Request success : " + success);
      }

      if (!success) {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("sloFailed"));
      }

      if (hostRole != null && hostRole.equals("SPRole")) {
         SPSingleLogout.postSingleLogoutSuccess(
            hostEntity, realm, request, response, userId, sloRequest, sloResponse, "urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
         );
      }
   }

   private static boolean checkSLOResponse(LogoutResponse sloResponse, String requestID) throws SAML2Exception {
      boolean success = false;
      String retCode = sloResponse.getStatus().getStatusCode().getValue();
      if (retCode.equalsIgnoreCase("urn:oasis:names:tc:SAML:2.0:status:Success")) {
         String inResponseTo = sloResponse.getInResponseTo();
         if (!inResponseTo.equals(requestID)) {
            SAML2SDKUtils.DEBUG.error("LogoutUtil.doSLOBySOAP LogoutResponse inResponseTo does not match Request ID.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("inResponseToNoMatch"));
         }

         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("LogoutUtil.doSLOBySOAP LogoutResponse inResponseTo matches LogoutRequest ID");
         }

         success = true;
      } else {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("LogoutUtil.doSLOBySOAP : return code : " + retCode);
         }

         success = false;
      }

      return success;
   }

   static LogoutResponse forwardToRemoteServer(LogoutRequest logoutReq, String remoteLogoutURL) {
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message("LogoutUtil.forwardToRemoteServer: remoteLogoutURL = " + remoteLogoutURL);
      }

      try {
         SOAPMessage resMsg = SAML2Utils.sendSOAPMessage(logoutReq.toXMLString(true, true), remoteLogoutURL, true);
         Element respElem = SAML2Utils.getSamlpElement(resMsg, "LogoutResponse");
         return ProtocolFactory.getInstance().createLogoutResponse(respElem);
      } catch (Exception ex) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("LogoutUtil.forwardToRemoteServer:", ex);
         }

         return null;
      }
   }

   static List<String> getSessionIndex(LogoutResponse logoutRes) {
      StatusDetail statusDetail = logoutRes.getStatus().getStatusDetail();
      if (statusDetail == null) {
         return null;
      }

      List<String> details = statusDetail.getAny();
      if (details != null && !details.isEmpty()) {
         List<String> sessionIndexList = new ArrayList<>();

         for (String detail : details) {
            Document doc = XMLUtils.toDOMDocument(detail, SAML2SDKUtils.DEBUG);
            Element elem = doc.getDocumentElement();
            String localName = elem.getLocalName();
            if ("SessionIndex".equals(localName)) {
               sessionIndexList.add(XMLUtils.getElementString(elem));
            }
         }

         return sessionIndexList;
      } else {
         return null;
      }
   }

   static void setSessionIndex(Status status, List<String> sessionIndex) {
      try {
         StatusDetail sd = ProtocolFactory.getInstance().createStatusDetail();
         status.setStatusDetail(sd);
         if (sessionIndex != null && !sessionIndex.isEmpty()) {
            List<String> details = new ArrayList<>();

            for (String si : sessionIndex) {
               SessionIndex sIndex = ProtocolFactory.getInstance().createSessionIndex(si);
               details.add(sIndex.toXMLString(true, true));
            }

            sd.setAny(details);
         }
      } catch (SAML2Exception e) {
         SAML2SDKUtils.DEBUG.error("LogoutUtil.setSessionIndex: ", e);
      }
   }

   public static String getSLOServiceLocation(List<EndpointType> sloList, String desiredBinding) {
      String classMethod = "LogoutUtil.getSLOserviceLocation: ";
      int n = sloList.size();
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message(classMethod + "Number of single logout services = " + n);
      }

      EndpointType slos = null;
      String location = null;
      String binding = null;

      for (int i = 0; i < n; i++) {
         slos = sloList.get(i);
         if (slos != null) {
            binding = slos.getBinding();
         }

         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(classMethod + "Single logout service binding = " + binding);
         }

         if (binding != null && binding.equals(desiredBinding)) {
            location = slos.getLocation();
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(classMethod + "Found the single logout service with the desired binding");
            }
            break;
         }
      }

      return location;
   }

   public static String getSLOResponseServiceLocation(List<EndpointType> sloList, String desiredBinding) {
      String classMethod = "LogoutUtil.getSLOResponseServiceLocation: ";
      int n = sloList.size();
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message(classMethod + "Number of single logout services = " + n);
      }

      EndpointType slos = null;
      String resLocation = null;
      String binding = null;

      for (int i = 0; i < n; i++) {
         slos = sloList.get(i);
         if (slos != null) {
            binding = slos.getBinding();
         }

         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(classMethod + "Single logout service binding = " + binding);
         }

         if (binding != null && binding.equals(desiredBinding)) {
            resLocation = slos.getResponseLocation();
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(classMethod + "Found the single logout service with the desired binding");
            }
            break;
         }
      }

      return resLocation;
   }

   private static Extensions createExtensions(List<String> extensionsList) throws SAML2Exception {
      Extensions extensions = null;
      if (extensionsList != null && !extensionsList.isEmpty()) {
         extensions = ProtocolFactory.getInstance().createExtensions();
         extensions.setAny(extensionsList);
      }

      return extensions;
   }

   public static LogoutResponse generateResponse(Status status, String inResponseTo, Issuer issuer) {
      if (status == null) {
         status = SAML2Utils.generateStatus("urn:oasis:names:tc:SAML:2.0:status:Success", SAML2Utils.BUNDLE.getString("requestSuccess"));
      }

      LogoutResponse logoutResponse = ProtocolFactory.getInstance().createLogoutResponse();
      String responseID = SAMLUtils.generateID();

      try {
         logoutResponse.setStatus(status);
         logoutResponse.setID(responseID);
         logoutResponse.setInResponseTo(inResponseTo);
         logoutResponse.setVersion("2.0");
         logoutResponse.setIssueInstant(new Date());
         logoutResponse.setIssuer(issuer);
      } catch (SAML2Exception e) {
         SAML2SDKUtils.DEBUG.error("Error in generating LogoutResponse.", e);
      }

      return logoutResponse;
   }

   private static void signSLORequest(LogoutRequest sloRequest, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception {
      signSLORequest(sloRequest, realm, hostEntity, hostEntityRole, remoteEntity, false);
   }

   private static void signSLORequest(
      LogoutRequest sloRequest, String realm, String hostEntity, String hostEntityRole, String remoteEntity, boolean includeCert
   ) throws SAML2Exception {
      String method = "signSLORequest : ";
      boolean needRequestSign = false;
      if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
         needRequestSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, "SPRole");
      } else {
         needRequestSign = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, "IDPRole");
      }

      if (!needRequestSign) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "SLORequest doesn't need to be signed.");
         }
      } else {
         String alias = SAML2Utils.getSigningCertAlias(realm, hostEntity, hostEntityRole);
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "realm is : " + realm);
            SAML2SDKUtils.DEBUG.message(method + "hostEntity is : " + hostEntity);
            SAML2SDKUtils.DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
            SAML2SDKUtils.DEBUG.message(method + "Cert Alias is : " + alias);
            SAML2SDKUtils.DEBUG.message(method + "SLO Request before sign : " + sloRequest.toXMLString(true, true));
         }

         PrivateKey signingKey = KEY_PROVIDER.getPrivateKey(alias);
         X509Certificate signingCert = null;
         if (includeCert) {
            signingCert = KEY_PROVIDER.getX509Certificate(alias);
         }

         if (signingKey != null) {
            sloRequest.sign(signingKey, signingCert);
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(method + "SLO Request after sign : " + sloRequest.toXMLString(true, true));
            }
         } else {
            SAML2SDKUtils.DEBUG.error("Incorrect configuration for Signing Certificate.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      }
   }

   public static boolean verifySLORequest(LogoutRequest sloRequest, String realm, String remoteEntity, String hostEntity, String hostEntityRole) throws SAML2Exception, SessionException {
      String method = "verifySLORequest : ";
      boolean needVerifySignature = SAML2Utils.getWantLogoutRequestSigned(realm, hostEntity, hostEntityRole);
      if (!needVerifySignature) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "SLORequest doesn't need to be verified.");
         }

         return true;
      } else {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "realm is : " + realm);
            SAML2SDKUtils.DEBUG.message(method + "remoteEntity is : " + remoteEntity);
            SAML2SDKUtils.DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
         }

         List<X509Certificate> signingCerts;
         if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
            SPSSODescriptorType spSSODesc = META_MANAGER.getSPSSODescriptor(realm, remoteEntity);
            signingCerts = KeyUtil.getVerificationCerts(spSSODesc, remoteEntity, "SPRole");
         } else {
            IDPSSODescriptorType idpSSODesc = META_MANAGER.getIDPSSODescriptor(realm, remoteEntity);
            signingCerts = KeyUtil.getVerificationCerts(idpSSODesc, remoteEntity, "IDPRole");
         }

         if (!signingCerts.isEmpty()) {
            boolean valid = sloRequest.isSignatureValid(signingCerts);
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(method + "Signature is : " + valid);
            }

            return valid;
         } else {
            SAML2SDKUtils.DEBUG.error("Incorrect configuration for Signing Certificate.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      }
   }

   public static void signSLOResponse(LogoutResponse sloResponse, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception {
      signSLOResponse(sloResponse, realm, hostEntity, hostEntityRole, remoteEntity, false);
   }

   static void signSLOResponse(LogoutResponse sloResponse, String realm, String hostEntity, String hostEntityRole, String remoteEntity, boolean includeCert) throws SAML2Exception {
      String method = "signSLOResponse : ";
      boolean needSignResponse = false;
      if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
         needSignResponse = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, "SPRole");
      } else {
         needSignResponse = SAML2Utils.getWantLogoutRequestSigned(realm, remoteEntity, "IDPRole");
      }

      if (!needSignResponse) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "SLOResponse doesn't need to be signed.");
         }
      } else {
         String alias = SAML2Utils.getSigningCertAlias(realm, hostEntity, hostEntityRole);
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "realm is : " + realm);
            SAML2SDKUtils.DEBUG.message(method + "hostEntity is : " + hostEntity);
            SAML2SDKUtils.DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
            SAML2SDKUtils.DEBUG.message(method + "Cert Alias is : " + alias);
         }

         PrivateKey signingKey = KEY_PROVIDER.getPrivateKey(alias);
         X509Certificate signingCert = null;
         if (includeCert) {
            signingCert = KEY_PROVIDER.getX509Certificate(alias);
         }

         if (signingKey != null) {
            sloResponse.sign(signingKey, signingCert);
         } else {
            SAML2SDKUtils.DEBUG.error("Incorrect configuration for Signing Certificate.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      }
   }

   public static boolean verifySLOResponse(LogoutResponse sloResponse, String realm, String remoteEntity, String hostEntity, String hostEntityRole) throws SAML2Exception, SessionException {
      String method = "verifySLOResponse : ";
      boolean needVerifySignature = SAML2Utils.getWantLogoutResponseSigned(realm, hostEntity, hostEntityRole);
      if (!needVerifySignature) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "SLOResponse doesn't need to be verified.");
         }

         return true;
      } else {
         boolean valid = false;
         List<X509Certificate> signingCerts;
         if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
            SPSSODescriptorType spSSODesc = META_MANAGER.getSPSSODescriptor(realm, remoteEntity);
            signingCerts = KeyUtil.getVerificationCerts(spSSODesc, remoteEntity, "SPRole");
         } else {
            IDPSSODescriptorType idpSSODesc = META_MANAGER.getIDPSSODescriptor(realm, remoteEntity);
            signingCerts = KeyUtil.getVerificationCerts(idpSSODesc, remoteEntity, "IDPRole");
         }

         if (!signingCerts.isEmpty()) {
            valid = sloResponse.isSignatureValid(signingCerts);
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(method + "Signature is : " + valid);
            }

            return valid;
         } else {
            SAML2SDKUtils.DEBUG.error("Incorrect configuration for Signing Certificate.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      }
   }

   static void setNameIDForSLORequest(LogoutRequest request, NameID nameID, String realm, String hostEntity, String hostEntityRole, String remoteEntity) throws SAML2Exception, SessionException {
      String method = "setNameIDForSLORequest: ";
      boolean needEncryptIt = false;
      if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
         needEncryptIt = SAML2Utils.getWantNameIDEncrypted(realm, remoteEntity, "SPRole");
      } else {
         needEncryptIt = SAML2Utils.getWantNameIDEncrypted(realm, remoteEntity, "IDPRole");
      }

      if (!needEncryptIt) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "NamID doesn't need to be encrypted.");
         }

         request.setNameID(nameID);
      } else {
         EncInfo encryptInfo = null;
         KeyDescriptorType keyDescriptor = null;
         if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
            RoleDescriptorType spSSODesc = META_MANAGER.getSPSSODescriptor(realm, remoteEntity);
            keyDescriptor = KeyUtil.getKeyDescriptor(spSSODesc, KeyTypes.ENCRYPTION);
            encryptInfo = KeyUtil.getEncInfo(spSSODesc, remoteEntity, "SPRole");
         } else {
            RoleDescriptorType idpSSODesc = META_MANAGER.getIDPSSODescriptor(realm, remoteEntity);
            keyDescriptor = KeyUtil.getKeyDescriptor(idpSSODesc, KeyTypes.ENCRYPTION);
            encryptInfo = KeyUtil.getEncInfo(idpSSODesc, remoteEntity, "IDPRole");
         }

         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "realm is : " + realm);
            SAML2SDKUtils.DEBUG.message(method + "hostEntity is : " + hostEntity);
            SAML2SDKUtils.DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
            SAML2SDKUtils.DEBUG.message(method + "remoteEntity is : " + remoteEntity);
         }

         if (encryptInfo == null) {
            SAML2SDKUtils.DEBUG.error("NO meta data for encrypt Info.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }

         X509Certificate certificate = KeyUtil.getCert(keyDescriptor);
         PublicKey recipientPublicKey = certificate.getPublicKey();
         EncryptedID encryptedID = nameID.encrypt(recipientPublicKey, encryptInfo.getDataEncAlgorithm(), encryptInfo.getDataEncStrength(), remoteEntity);
         request.setEncryptedID(encryptedID);
      }
   }

   static NameID getNameIDFromSLORequest(LogoutRequest request, String realm, String hostEntity, String hostEntityRole) throws SAML2Exception {
      String method = "getNameIDFromSLORequest: ";
      String alias = null;
      boolean needDecryptIt = SAML2Utils.getWantNameIDEncrypted(realm, hostEntity, hostEntityRole);
      if (!needDecryptIt) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "NamID doesn't need to be decrypted.");
         }

         return request.getNameID();
      } else {
         alias = SAML2Utils.getEncryptionCertAlias(realm, hostEntity, hostEntityRole);
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "realm is : " + realm);
            SAML2SDKUtils.DEBUG.message(method + "hostEntity is : " + hostEntity);
            SAML2SDKUtils.DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
            SAML2SDKUtils.DEBUG.message(method + "Cert Alias is : " + alias);
         }

         PrivateKey privateKey = KEY_PROVIDER.getPrivateKey(alias);
         EncryptedID encryptedID = request.getEncryptedID();
         return encryptedID.decrypt(privateKey);
      }
   }

   public static void sendSLOResponse(
      HttpServletRequest request,
      HttpServletResponse response,
      LogoutResponse sloResponse,
      String sloURL,
      String relayState,
      String realm,
      String hostEntity,
      String hostEntityRole,
      String remoteEntity,
      String binding
   ) throws SAML2Exception {
      if ("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".equals(binding)) {
         sendSLOResponsePost(request, response, sloResponse, sloURL, relayState, realm, hostEntity, hostEntityRole, remoteEntity);
      } else {
         sendSLOResponseRedirect(response, sloResponse, sloURL, relayState, realm, hostEntity, hostEntityRole, remoteEntity);
      }
   }

   private static void sendSLOResponsePost(
      HttpServletRequest request,
      HttpServletResponse response,
      LogoutResponse sloResponse,
      String sloURL,
      String relayState,
      String realm,
      String hostEntity,
      String hostEntityRole,
      String remoteEntity
   ) throws SAML2Exception {
      signSLOResponse(sloResponse, realm, hostEntity, hostEntityRole, remoteEntity);
      String logoutResponseStr = sloResponse.toXMLString(true, true);
      String encMsg = SAML2Utils.encodeForPOST(logoutResponseStr);

      try {
         SAML2Utils.postToTarget(request, response, "SAMLResponse", encMsg, "RelayState", relayState, sloURL);
      } catch (IOException ioe) {
         SAML2SDKUtils.DEBUG.error("LogoutUtil.sendSLOResponsePost:", ioe);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorPostingLogoutResponse"));
      }
   }

   private static void sendSLOResponseRedirect(
      HttpServletResponse response,
      LogoutResponse sloResponse,
      String sloURL,
      String relayState,
      String realm,
      String hostEntity,
      String hostEntityRole,
      String remoteEntity
   ) throws SAML2Exception {
      try {
         String logoutResXMLString = sloResponse.toXMLString(true, true);
         String encodedXML = SAML2Utils.encodeForRedirect(logoutResXMLString);
         StringBuilder queryString = new StringBuilder().append("SAMLResponse").append("=").append(encodedXML);
         if (relayState != null && !relayState.isEmpty() && relayState.getBytes(StandardCharsets.UTF_8).length <= 80) {
            queryString.append("&").append("RelayState").append("=").append(URLEncDec.encode(relayState));
         }

         boolean needToSign = false;
         if (hostEntityRole.equalsIgnoreCase("IDPRole")) {
            needToSign = SAML2Utils.getWantLogoutResponseSigned(realm, remoteEntity, "SPRole");
         } else {
            needToSign = SAML2Utils.getWantLogoutResponseSigned(realm, remoteEntity, "IDPRole");
         }

         String signedQueryString = queryString.toString();
         if (needToSign) {
            signedQueryString = SAML2Utils.signQueryString(signedQueryString, realm, hostEntity, hostEntityRole);
         }

         String redirectURL = sloURL + (sloURL.contains("?") ? "&" : "?") + signedQueryString;
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("redirectURL :" + redirectURL);
         }

         String[] data = new String[]{sloURL};
         LogUtil.access(Level.INFO, "REDIRECT_TO_SP", data, null);
         response.sendRedirect(redirectURL);
      } catch (Exception e) {
         SAML2SDKUtils.DEBUG.error("Exception :", e);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorRedirectingLogoutResponse"));
      }
   }

   public static String getSLOBindingInfo(HttpServletRequest request, String metaAlias, String hostEntityRole, String remoteEntityID) throws SAML2Exception {
      String binding = request.getParameter("binding");

      try {
         if (binding == null) {
            String realm = SAML2MetaUtils.getRealmByMetaAlias(metaAlias);
            EndpointType sloService = getSLOServiceElement(realm, remoteEntityID, hostEntityRole, null);
            if (sloService != null) {
               binding = sloService.getBinding();
            }
         }
      } catch (SessionException e) {
         SAML2SDKUtils.DEBUG.error("Invalid SSOToken", e);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      if (binding == null) {
         SAML2SDKUtils.DEBUG.error("Incorrect configuration for SingleLogout Service.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      } else {
         return binding;
      }
   }

   private static EndpointType getSLOServiceElement(String realm, String entityID, String hostEntityRole, String binding) throws SAML2MetaException, SessionException, SAML2Exception {
      EndpointType sloService = null;
      String method = "getSLOServiceElement: ";
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message(method + "Realm : " + realm);
         SAML2SDKUtils.DEBUG.message(method + "Entity ID : " + entityID);
         SAML2SDKUtils.DEBUG.message(method + "Host Entity Role : " + hostEntityRole);
      }

      if (hostEntityRole.equalsIgnoreCase("SPRole")) {
         sloService = getIDPSLOConfig(realm, entityID, binding);
      } else {
         if (!hostEntityRole.equalsIgnoreCase("IDPRole")) {
            SAML2SDKUtils.DEBUG.error("Hosted Entity role is missing .");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullIDPEntityID"));
         }

         sloService = getSPSLOConfig(realm, entityID, binding);
      }

      return sloService;
   }

   private static EndpointType getIDPSLOConfig(String realm, String entityId, String binding) throws SAML2MetaException {
      EndpointType slo = null;
      IDPSSODescriptorType idpSSODesc = META_MANAGER.getIDPSSODescriptor(realm, entityId);
      if (idpSSODesc == null) {
         SAML2SDKUtils.DEBUG.error("Identity Provider SSO config is missing.");
         return null;
      }

      List<EndpointType> list = idpSSODesc.getSingleLogoutService();
      if (list != null && !list.isEmpty()) {
         if (binding == null) {
            return list.getFirst();
         }

         for (EndpointType singleLogoutServiceElement : list) {
            slo = singleLogoutServiceElement;
            if (binding.equalsIgnoreCase(slo.getBinding())) {
               break;
            }
         }
      }

      return slo;
   }

   private static EndpointType getSPSLOConfig(String realm, String entityId, String binding) throws SAML2MetaException {
      EndpointType slo = null;
      SPSSODescriptorType spSSODesc = META_MANAGER.getSPSSODescriptor(realm, entityId);
      if (spSSODesc == null) {
         return null;
      }

      List<EndpointType> list = spSSODesc.getSingleLogoutService();
      if (list != null && !list.isEmpty()) {
         if (binding == null) {
            return list.getFirst();
         }

         for (EndpointType singleLogoutServiceElement : list) {
            slo = singleLogoutServiceElement;
            if (binding.equalsIgnoreCase(slo.getBinding())) {
               break;
            }
         }
      }

      return slo;
   }

   public static List<String> getExtensionsList(Map<String, ?> paramsMap) {
      return null;
   }

   private static void doSLOByPOST(String sloRequestXMLString, String sloURL, String relayState, HttpServletRequest request, HttpServletResponse response) throws SAML2Exception {
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message("LogoutUtil.doSLOByPOST : SLORequestXML: " + sloRequestXMLString + "\nPOSTURL : " + sloURL);
         SAML2SDKUtils.DEBUG.message("LogoutUtil.doSLOByPOST : relayState : " + sloRequestXMLString + "\nPOSTURL : " + relayState);
      }

      String encMsg = SAML2Utils.encodeForPOST(sloRequestXMLString);

      try {
         SAML2Utils.postToTarget(request, response, "SAMLRequest", encMsg, "RelayState", relayState, sloURL);
      } catch (Exception e) {
         SAML2SDKUtils.DEBUG.error("LogoutUtil.doSLOByPOST:", e);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("postToTargetFailed"));
      }
   }

   static LogoutResponse getLogoutResponseFromPost(String samlResponse) throws SAML2Exception {
      if (samlResponse == null) {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("missingLogoutResponse"));
      }

      LogoutResponse resp = null;
      ByteArrayInputStream bis = null;

      try {
         byte[] raw = Base64.decode(samlResponse);
         if (raw != null) {
            bis = new ByteArrayInputStream(raw);
            Document doc = XMLUtils.toDOMDocument(bis, SAML2SDKUtils.DEBUG);
            if (doc != null) {
               resp = ProtocolFactory.getInstance().createLogoutResponse(doc.getDocumentElement());
            }
         }
      } catch (Exception e) {
         SAML2SDKUtils.DEBUG.error("LogoutUtil.getLogoutResponseFromPost:", e);
      } finally {
         if (bis != null) {
            try {
               bis.close();
            } catch (Exception ie) {
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message("LogoutUtil.getLogoutResponseFromPost:", ie);
               }
            }
         }
      }

      if (resp == null) {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorGettingLogoutResponse"));
      } else {
         return resp;
      }
   }

   static LogoutRequest getLogoutRequestFromPost(String samlRequest) throws SAML2Exception {
      if (samlRequest == null) {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("missingLogoutRequest"));
      }

      LogoutRequest req = null;
      ByteArrayInputStream bis = null;

      try {
         byte[] raw = Base64.decode(samlRequest);
         if (raw != null) {
            bis = new ByteArrayInputStream(raw);
            Document doc = XMLUtils.toDOMDocument(bis, SAML2SDKUtils.DEBUG);
            if (doc != null) {
               req = ProtocolFactory.getInstance().createLogoutRequest(doc.getDocumentElement());
            }
         }
      } catch (Exception e) {
         SAML2SDKUtils.DEBUG.error("LogoutUtil.getLogoutRequestFromPost:", e);
      } finally {
         if (bis != null) {
            try {
               bis.close();
            } catch (Exception ie) {
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message("LogoutUtil.getLogoutRequestFromPost:", ie);
               }
            }
         }
      }

      if (req == null) {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorGettingLogoutRequest"));
      } else {
         return req;
      }
   }
}
