package com.sun.identity.saml2.common;

import com.airlock.iam.openfedlib.saml2.XjcHelper;
import com.airlock.iam.openfedlib.saml2.servlet.Saml2ResponseBodyWriter;
import com.airlock.iam.opensso.jaxb.entityconfig.BaseConfigType;
import com.airlock.iam.opensso.jaxb.metadata.AffiliationDescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.IDPSSODescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.IndexedEndpointType;
import com.airlock.iam.opensso.jaxb.metadata.SPSSODescriptorType;
import com.sun.identity.common.HttpURLConnectionManager;
import com.sun.identity.common.SystemConfigurationUtil;
import com.sun.identity.plugin.datastore.DataStoreProvider;
import com.sun.identity.plugin.datastore.DataStoreProviderException;
import com.sun.identity.plugin.datastore.DataStoreProviderManager;
import com.sun.identity.plugin.session.SessionException;
import com.sun.identity.plugin.session.SessionManager;
import com.sun.identity.saml.xmlsig.KeyProvider;
import com.sun.identity.saml2.assertion.Assertion;
import com.sun.identity.saml2.assertion.AssertionFactory;
import com.sun.identity.saml2.assertion.Attribute;
import com.sun.identity.saml2.assertion.AudienceRestriction;
import com.sun.identity.saml2.assertion.AuthnStatement;
import com.sun.identity.saml2.assertion.Conditions;
import com.sun.identity.saml2.assertion.EncryptedAssertion;
import com.sun.identity.saml2.assertion.Issuer;
import com.sun.identity.saml2.assertion.NameID;
import com.sun.identity.saml2.assertion.Subject;
import com.sun.identity.saml2.assertion.SubjectConfirmation;
import com.sun.identity.saml2.assertion.SubjectConfirmationData;
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.plugins.DefaultSPAuthnContextMapper;
import com.sun.identity.saml2.plugins.IDPAccountMapper;
import com.sun.identity.saml2.plugins.SAML2ServiceProviderAdapter;
import com.sun.identity.saml2.plugins.SPAuthnContextMapper;
import com.sun.identity.saml2.profile.IDPCache;
import com.sun.identity.saml2.profile.SPCache;
import com.sun.identity.saml2.protocol.AuthnRequest;
import com.sun.identity.saml2.protocol.ProtocolFactory;
import com.sun.identity.saml2.protocol.RequestAbstract;
import com.sun.identity.saml2.protocol.RequestedAuthnContext;
import com.sun.identity.saml2.protocol.Response;
import com.sun.identity.saml2.protocol.Status;
import com.sun.identity.saml2.protocol.StatusCode;
import com.sun.identity.shared.configuration.SystemPropertiesManager;
import com.sun.identity.shared.encode.Base64;
import com.sun.identity.shared.encode.URLEncDec;
import com.sun.identity.shared.xml.XMLUtils;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.xml.soap.Detail;
import jakarta.xml.soap.MessageFactory;
import jakarta.xml.soap.MimeHeader;
import jakarta.xml.soap.MimeHeaders;
import jakarta.xml.soap.Name;
import jakarta.xml.soap.SOAPBody;
import jakarta.xml.soap.SOAPConnection;
import jakarta.xml.soap.SOAPConnectionFactory;
import jakarta.xml.soap.SOAPElement;
import jakarta.xml.soap.SOAPEnvelope;
import jakarta.xml.soap.SOAPException;
import jakarta.xml.soap.SOAPFault;
import jakarta.xml.soap.SOAPMessage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class SAML2Utils extends SAML2SDKUtils {
   public static final SOAPConnectionFactory CONNECTION_FACTORY = createConnectionFactory();
   public static final MessageFactory MESSAGE_FACTORY = createMessageFactory();
   private static final String REQUEST_ATTRIBUTE_CSP_NONCE = "cspNonce";
   private static final String SOAP_1_1_MIME_TYPE = "text/xml";
   private static final SAML2MetaManager META_MANAGER = createMetaManager();
   private static final AssertionFactory ASSERTION_FACTORY = AssertionFactory.getInstance();
   private static final KeyProvider KEY_PROVIDER = KeyUtil.getKeyProviderInstance();
   private static final String SERVER_PROTOCOL = SystemPropertiesManager.get("com.iplanet.am.server.protocol");
   private static final String SERVER_HOST = SystemPropertiesManager.get("com.iplanet.am.server.host");
   private static final String SERVER_URI = SystemPropertiesManager.get("com.iplanet.am.services.deploymentDescriptor");
   private static final int SERVER_PORT = parseServerPort(SystemPropertiesManager.get("com.iplanet.am.server.port"));
   private static final SecureRandom RANDOM = new SecureRandom();
   private static final String BUFFER_LEN = (String)SAML2ConfigService.getAttribute("bufferLength");

   @Nullable
   private static SOAPConnectionFactory createConnectionFactory() {
      try {
         return SOAPConnectionFactory.newInstance();
      } catch (SOAPException se) {
         DEBUG.error("Unable to obtain SOAPConnectionFactory.", se);
         return null;
      }
   }

   @Nullable
   private static MessageFactory createMessageFactory() {
      try {
         return MessageFactory.newInstance();
      } catch (SOAPException se) {
         DEBUG.error("SAML2Utils: Unable to create SOAP MessageFactory", se);
         return null;
      }
   }

   private static int parseServerPort(String portStr) {
      if (portStr == null) {
         return 0;
      }

      try {
         return Integer.parseInt(portStr);
      } catch (NumberFormatException nfe) {
         DEBUG.error("Unable to parse port " + portStr, nfe);
         return 0;
      }
   }

   private static SAML2MetaManager createMetaManager() {
      try {
         return new SAML2MetaManager();
      } catch (SAML2MetaException sme) {
         DEBUG.error("Error retrieving metadata", sme);
         return null;
      }
   }

   public static boolean isSAML2FailOverEnabled() {
      boolean failOver = false;
      String enableFailOver = (String)SAML2ConfigService.getAttribute("failOverEnabled");
      if (enableFailOver != null && enableFailOver.equalsIgnoreCase("true")) {
         failOver = true;
      }

      return failOver;
   }

   public static Map<String, Object> verifyResponse(
      HttpServletRequest httpRequest, HttpServletResponse httpResponse, Response response, String orgName, String hostEntityId, String profileBinding
   ) throws SAML2Exception {
      String method = "SAML2Utils.verifyResponse:";
      if (response != null && orgName != null && !orgName.isEmpty()) {
         String respID = response.getID();
         AuthnRequest authnRequest = null;
         String inRespToResp = response.getInResponseTo();
         if (inRespToResp != null && !inRespToResp.isEmpty()) {
            authnRequest = SPCache.requestHash().get(inRespToResp);
            if (authnRequest == null) {
               if (DEBUG.messageEnabled()) {
                  DEBUG.message(method + "InResponseTo attribute in Response is invalid: " + inRespToResp);
               }

               String[] data = new String[]{respID};
               LogUtil.error(Level.INFO, "INVALID_INRESPONSETO_RESPONSE", data, null);
               throw new SAML2Exception(BUNDLE.getString("invalidInResponseToInResponse"));
            }
         }

         SAML2ServiceProviderAdapter spAdapter = getSPAdapterClass(hostEntityId, orgName);
         if (spAdapter != null) {
            spAdapter.preSingleSignOnProcess(hostEntityId, orgName, httpRequest, httpResponse, authnRequest, response, profileBinding);
         }

         String idpEntityId = null;
         Issuer respIssuer = response.getIssuer();
         if (respIssuer != null) {
            if (!isSourceSiteValid(respIssuer, orgName, hostEntityId)) {
               if (DEBUG.messageEnabled()) {
                  DEBUG.message(method + "Issuer in Response is not valid.");
               }

               String[] data = new String[]{hostEntityId, orgName, respID};
               LogUtil.error(Level.INFO, "INVALID_ISSUER_RESPONSE", data, null);
               throw new SAML2Exception(BUNDLE.getString("invalidIssuerInResponse"));
            }

            idpEntityId = respIssuer.getValue();
         }

         Status status = response.getStatus();
         if (status == null || !status.getStatusCode().getValue().equals("urn:oasis:names:tc:SAML:2.0:status:Success")) {
            String statusCode = status == null ? "" : status.getStatusCode().getValue();
            if (DEBUG.messageEnabled()) {
               DEBUG.message(method + "Response's status code is not success." + statusCode);
            }

            String[] data = new String[]{respID, ""};
            if (LogUtil.isErrorLoggable(Level.FINE)) {
               data[1] = statusCode;
            }

            LogUtil.error(Level.INFO, "WRONG_STATUS_CODE", data, null);
            throw new SAML2Exception(BUNDLE.getString("invalidStatusCodeInResponse"));
         } else {
            if (META_MANAGER == null) {
               throw new SAML2Exception(BUNDLE.getString("nullMetaManager"));
            }

            BaseConfigType spConfig = META_MANAGER.getSPSSOConfig(orgName, hostEntityId);
            SPSSODescriptorType spDesc = META_MANAGER.getSPSSODescriptor(orgName, hostEntityId);
            boolean needAssertionEncrypted = false;
            String assertionEncryptedAttr = getAttributeValueFromSPSSOConfig(spConfig, "wantAssertionEncrypted");
            if (assertionEncryptedAttr != null && assertionEncryptedAttr.equals("true")) {
               needAssertionEncrypted = true;
            }

            boolean needAssertionSigned = profileBinding != null && profileBinding.equals("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST");
            if (!needAssertionSigned) {
               needAssertionSigned = XjcHelper.mapBooleanWrapperToPrimitiveWithFalseAsDefault(spDesc.isWantAssertionsSigned());
            }

            if (needAssertionSigned) {
               boolean wantPostResponseSigned = wantPOSTResponseSigned(orgName, hostEntityId, "SPRole");
               if ("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".equals(profileBinding)) {
                  if (DEBUG.messageEnabled()) {
                     DEBUG.message(method + "binding is :" + profileBinding);
                  }

                  if (wantPostResponseSigned) {
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message(method + "signResponse  :" + wantPostResponseSigned);
                     }

                     needAssertionSigned = false;
                  } else if (response.isSigned()) {
                     IDPSSODescriptorType idpSSODescriptor = null;

                     try {
                        idpSSODescriptor = META_MANAGER.getIDPSSODescriptor(orgName, idpEntityId);
                     } catch (SAML2MetaException sme) {
                        DEBUG.error(method, sme);
                     }

                     if (idpSSODescriptor != null) {
                        List<X509Certificate> idpCert = KeyUtil.getVerificationCerts(idpSSODescriptor, idpEntityId, "IDPRole");
                        if (!idpCert.isEmpty() && response.isSignatureValid(idpCert)) {
                           needAssertionSigned = false;
                        }
                     }
                  }
               }
            }

            List<Assertion> assertions = response.getAssertion();
            if (needAssertionEncrypted && assertions != null && !assertions.isEmpty()) {
               String[] data = new String[]{respID};
               LogUtil.error(Level.INFO, "ASSERTION_NOT_ENCRYPTED", data, null);
               throw new SAML2Exception(BUNDLE.getString("assertionNotEncrypted"));
            }

            PrivateKey decryptionKey = null;
            List<EncryptedAssertion> encAssertions = response.getEncryptedAssertion();
            if (encAssertions != null) {
               for (EncryptedAssertion encAssertion : encAssertions) {
                  if (decryptionKey == null) {
                     decryptionKey = KeyUtil.getDecryptionKey(spConfig);
                  }

                  Assertion assertion = encAssertion.decrypt(decryptionKey);
                  if (assertions == null) {
                     assertions = new ArrayList<>();
                  }

                  assertions.add(assertion);
               }
            }

            if (assertions != null && !assertions.isEmpty()) {
               Map<String, Object> smap = null;
               IDPSSODescriptorType idp = null;
               List<X509Certificate> certs = Collections.emptyList();

               for (Assertion assertion : assertions) {
                  String assertionID = assertion.getID();
                  Issuer issuer = assertion.getIssuer();
                  if (!isSourceSiteValid(issuer, orgName, hostEntityId)) {
                     DEBUG.error("assertion's source site is not valid.");
                     String[] data = new String[]{assertionID};
                     LogUtil.error(Level.INFO, "INVALID_ISSUER_ASSERTION", data, null);
                     throw new SAML2Exception(BUNDLE.getString("invalidIssuerInAssertion"));
                  }

                  if (idpEntityId == null) {
                     idpEntityId = issuer.getValue();
                  } else if (!idpEntityId.equals(issuer.getValue())) {
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message(method + "Issuer in Assertion doesn't match the Issuer in Response or other Assertions in the Response.");
                     }

                     String[] data = new String[]{assertionID};
                     LogUtil.error(Level.INFO, "MISMATCH_ISSUER_ASSERTION", data, null);
                     throw new SAML2Exception(BUNDLE.getString("mismatchIssuer"));
                  }

                  if (needAssertionSigned) {
                     if (certs.isEmpty()) {
                        idp = META_MANAGER.getIDPSSODescriptor(orgName, idpEntityId);
                        certs = KeyUtil.getVerificationCerts(idp, idpEntityId, "IDPRole");
                     }

                     if (!assertion.isSigned() || !assertion.isSignatureValid(certs)) {
                        DEBUG.error(method + "Assertion is not signed or signature is not valid.");
                        String[] data = new String[]{assertionID};
                        LogUtil.error(Level.INFO, "INVALID_SIGNATURE_ASSERTION", data, null);
                        throw new SAML2Exception(BUNDLE.getString("invalidSignatureOnAssertion"));
                     }
                  }

                  List<AuthnStatement> authnStmts = assertion.getAuthnStatements();
                  if (authnStmts != null && !authnStmts.isEmpty()) {
                     Subject subject = assertion.getSubject();
                     if (subject != null) {
                        List<SubjectConfirmation> subjectConfirms = subject.getSubjectConfirmation();
                        if (subjectConfirms != null && !subjectConfirms.isEmpty()) {
                           Map<String, Object> bearerMap = isBearerSubjectConfirmation(subjectConfirms, inRespToResp, spDesc, spConfig, assertionID);
                           if ((Boolean)bearerMap.get("isBearer")) {
                              boolean foundAssertion = SPCache.assertionByIDCache().contains(assertionID);
                              if (!foundAssertion && isSAML2FailOverEnabled()) {
                                 try {
                                    if (SAML2Repository.getInstance().retrieve(assertionID) != null) {
                                       foundAssertion = true;
                                    }
                                 } catch (Exception ae) {
                                    if (DEBUG.messageEnabled()) {
                                       DEBUG.message("Session not found in JMQSAML2Repository.");
                                    }
                                 }
                              }

                              if (foundAssertion) {
                                 DEBUG.error("Bearer Assertion is one time use only!");
                                 throw new SAML2Exception(BUNDLE.getString("usedBearAssertion"));
                              }

                              checkAudience(assertion.getConditions(), hostEntityId, assertionID);
                              if (smap == null) {
                                 smap = fillMap(
                                    authnStmts,
                                    subject,
                                    assertion,
                                    assertions,
                                    authnRequest,
                                    inRespToResp,
                                    orgName,
                                    hostEntityId,
                                    idpEntityId,
                                    spConfig,
                                    (Date)bearerMap.get("NotOnOrAfter")
                                 );
                              }
                           }
                        }
                     }
                  }
               }

               if (smap == null) {
                  DEBUG.error("No Authentication Assertion in Response.");
                  throw new SAML2Exception(BUNDLE.getString("missingAuthnAssertion"));
               } else {
                  return smap;
               }
            } else {
               if (DEBUG.messageEnabled()) {
                  DEBUG.message(method + "no assertion in the Response.");
               }

               String[] data = new String[]{respID};
               LogUtil.error(Level.INFO, "MISSING_ASSERTION", data, null);
               throw new SAML2Exception(BUNDLE.getString("missingAssertion"));
            }
         }
      } else {
         if (DEBUG.messageEnabled()) {
            DEBUG.message(method + "response or orgName is null.");
         }

         throw new SAML2Exception(BUNDLE.getString("nullInput"));
      }
   }

   private static Map<String, Object> isBearerSubjectConfirmation(
      List<SubjectConfirmation> subjectConfirms, String inRespToResponse, SPSSODescriptorType spDesc, BaseConfigType spConfig, String assertionID
   ) throws SAML2Exception {
      String method = "SAML2Utils.isBearerSubjectConfirmation:";
      Map<String, Object> retMap = new HashMap<>();
      boolean hasBearer = false;

      for (SubjectConfirmation confirm : subjectConfirms) {
         if (confirm != null && confirm.getMethod() != null && confirm.getMethod().equals("urn:oasis:names:tc:SAML:2.0:cm:bearer")) {
            SubjectConfirmationData subjectConfData = confirm.getSubjectConfirmationData();
            if (subjectConfData == null) {
               if (DEBUG.messageEnabled()) {
                  DEBUG.message(method + "missing SubjectConfirmationData.");
               }

               String[] data = new String[]{assertionID};
               LogUtil.error(Level.INFO, "MISSING_SUBJECT_COMFIRMATION_DATA", data, null);
               throw new SAML2Exception(BUNDLE.getString("missingSubjectConfirmationData"));
            }

            String recipient = subjectConfData.getRecipient();
            if (recipient != null && !recipient.isEmpty()) {
               boolean foundMatch = false;

               for (IndexedEndpointType acs : spDesc.getAssertionConsumerService()) {
                  if (recipient.equals(acs.getLocation())) {
                     foundMatch = true;
                     break;
                  }
               }

               if (!foundMatch) {
                  if (DEBUG.messageEnabled()) {
                     DEBUG.message(method + "this sp is not the intended recipient.");
                  }

                  String[] data = new String[]{assertionID, recipient};
                  LogUtil.error(Level.INFO, "WRONG_RECIPIENT", data, null);
                  throw new SAML2Exception(BUNDLE.getString("wrongRecipient"));
               }

               int timeskew = 300;
               String timeskewStr = getAttributeValueFromSPSSOConfig(spConfig, "assertionTimeSkew");
               if (timeskewStr != null && !timeskewStr.trim().isEmpty()) {
                  timeskew = Integer.parseInt(timeskewStr);
                  if (timeskew < 0) {
                     timeskew = 300;
                  }
               }

               if (DEBUG.messageEnabled()) {
                  DEBUG.message(method + "timeskew = " + timeskew);
               }

               Date notOnOrAfter = subjectConfData.getNotOnOrAfter();
               if (notOnOrAfter != null && notOnOrAfter.getTime() + timeskew * 1000L >= System.currentTimeMillis()) {
                  retMap.put("NotOnOrAfter", notOnOrAfter);
                  if (subjectConfData.getNotBefore() != null) {
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message(method + "SubjectConfirmationData included NotBefore.");
                     }

                     String[] data = new String[]{assertionID};
                     LogUtil.error(Level.INFO, "CONTAINED_NOT_BEFORE", data, null);
                     throw new SAML2Exception(BUNDLE.getString("containedNotBefore"));
                  }

                  String inRespTo = subjectConfData.getInResponseTo();
                  if (inRespTo != null && !inRespTo.isEmpty()) {
                     if (!inRespTo.equals(inRespToResponse)) {
                        if (DEBUG.messageEnabled()) {
                           DEBUG.message(method + "InResponseTo in Assertion is different from the one in Response.");
                        }

                        String[] data = new String[]{assertionID};
                        LogUtil.error(Level.INFO, "WRONG_INRESPONSETO_ASSERTION", data, null);
                        throw new SAML2Exception(BUNDLE.getString("wrongInResponseToInAssertion"));
                     }
                  } else if (inRespToResponse != null && !inRespToResponse.isEmpty()) {
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message(method + "Assertion doesn't contain InResponseTo, but Response does.");
                     }

                     String[] data = new String[]{assertionID};
                     LogUtil.error(Level.INFO, "WRONG_INRESPONSETO_ASSERTION", data, null);
                     throw new SAML2Exception(BUNDLE.getString("wrongInResponseToInAssertion"));
                  }

                  hasBearer = true;
                  break;
               }

               if (DEBUG.messageEnabled()) {
                  DEBUG.message(method + "Time in SubjectConfirmationData of Assertion:" + assertionID + " is invalid.");
               }

               String[] data = new String[]{assertionID};
               LogUtil.error(Level.INFO, "INVALID_TIME_SUBJECT_CONFIRMATION_DATA", data, null);
               throw new SAML2Exception(BUNDLE.getString("invalidTimeOnSubjectConfirmationData"));
            }

            if (DEBUG.messageEnabled()) {
               DEBUG.message(method + "missing Recipient in Assertion.");
            }

            String[] data = new String[]{assertionID};
            LogUtil.error(Level.INFO, "MISSING_RECIPIENT", data, null);
            throw new SAML2Exception(BUNDLE.getString("missingRecipient"));
         }
      }

      retMap.put("isBearer", hasBearer);
      return retMap;
   }

   private static void checkAudience(Conditions conds, String hostEntityId, String assertionID) throws SAML2Exception {
      String method = "SAML2Utils.checkAudience:";
      if (conds == null) {
         if (DEBUG.messageEnabled()) {
            DEBUG.message(method + "Conditions is missing from Assertion.");
         }

         String[] data = new String[]{assertionID};
         LogUtil.error(Level.INFO, "MISSING_CONDITIONS", data, null);
         throw new SAML2Exception(BUNDLE.getString("missingConditions"));
      } else {
         List<AudienceRestriction> restrictions = conds.getAudienceRestrictions();
         if (restrictions == null) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message(method + "missing AudienceRestriction.");
            }

            String[] data = new String[]{assertionID};
            LogUtil.error(Level.INFO, "MISSING_AUDIENCE_RESTRICTION", data, null);
            throw new SAML2Exception(BUNDLE.getString("missingAudienceRestriction"));
         } else {
            Iterator<AudienceRestriction> restIter = restrictions.iterator();
            boolean found = false;

            while (restIter.hasNext()) {
               List<String> audienceList = restIter.next().getAudience();
               if (audienceList.contains(hostEntityId)) {
                  found = true;
                  break;
               }
            }

            if (!found) {
               DEBUG.warning(
                  method
                     + "This SP (\""
                     + hostEntityId
                     + "\") is not in the list of the intended audiences: "
                     + restrictions.stream().flatMap(ar -> ar.getAudience().stream()).map(aud -> "\"" + aud + "\"").collect(Collectors.joining(", "))
               );
               throw new SAML2Exception(BUNDLE.getString("audienceNotMatch"));
            }
         }
      }
   }

   private static Map<String, Object> fillMap(
      List<AuthnStatement> authnStmts,
      Subject subject,
      Assertion assertion,
      List<Assertion> assertions,
      AuthnRequest authnRequest,
      String inRespToResp,
      String orgName,
      String hostEntityId,
      String idpEntityId,
      BaseConfigType spConfig,
      Date notOnOrAfterTime
   ) throws SAML2Exception {
      AuthnStatement authnStmt = authnStmts.getFirst();
      String mapperClass = getAttributeValueFromSPSSOConfig(spConfig, "spAuthncontextMapper");
      SPAuthnContextMapper mapper = getSPAuthnContextMapper(orgName, hostEntityId, mapperClass);
      RequestedAuthnContext reqContext = null;
      if (authnRequest != null) {
         reqContext = authnRequest.getRequestedAuthnContext();
      }

      int authLevel = mapper.getAuthLevel(reqContext, authnStmt.getAuthnContext(), orgName, hostEntityId, idpEntityId);
      String sessionIndex = authnStmt.getSessionIndex();
      Date sessionNotOnOrAfter = authnStmt.getSessionNotOnOrAfter();
      Map<String, Object> smap = new HashMap<>();
      smap.put("Subject", subject);
      smap.put("assertion", assertion);
      smap.put("assertions", assertions);
      if (authnRequest != null) {
         smap.put("AuthnRequest", authnRequest);
      }

      String[] data = new String[]{assertion.getID(), "", ""};
      if (LogUtil.isAccessLoggable(Level.FINE)) {
         data[1] = subject.toXMLString();
      }

      if (sessionIndex != null && !sessionIndex.isEmpty()) {
         data[2] = sessionIndex;
         smap.put("SessionIndex", sessionIndex);
      }

      if (authLevel >= 0) {
         smap.put("AuthLevel", authLevel);
      }

      if (sessionNotOnOrAfter != null) {
         long maxSessionTime = (sessionNotOnOrAfter.getTime() - System.currentTimeMillis()) / 60000L;
         if (maxSessionTime > 0L) {
            smap.put("maxSessionTime", maxSessionTime);
         }
      }

      if (inRespToResp != null && !inRespToResp.isEmpty()) {
         smap.put("inResponseTo", inRespToResp);
      }

      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Utils.fillMap: Found valid authentication assertion.");
      }

      if (notOnOrAfterTime != null) {
         smap.put("NotOnOrAfter", notOnOrAfterTime.getTime());
      }

      LogUtil.access(Level.INFO, "FOUND_AUTHN_ASSERTION", data, null);
      return smap;
   }

   public static String getAttributeValueFromSPSSOConfig(BaseConfigType config, String attrName) {
      String result = null;
      if (config == null) {
         return null;
      }

      Map<String, List<String>> attrs = SAML2MetaUtils.getAttributes(config);
      List<String> value = attrs.get(attrName);
      if (value != null && !value.isEmpty()) {
         result = value.iterator().next().trim();
      }

      return result;
   }

   public static boolean isFedInfoExists(String userName, String hostEntityID, String remoteEntityId, NameID nameID) {
      boolean exists = false;
      if (userName != null && hostEntityID != null && remoteEntityId != null && nameID != null) {
         try {
            NameIDInfo info = AccountUtils.getAccountFederation(userName, hostEntityID, remoteEntityId);
            if (info != null && info.getNameIDValue().equals(nameID.getValue())) {
               exists = true;
            }
         } catch (SAML2Exception se) {
            DEBUG.error("Failed to get DataStoreProvider " + se);
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils:isFedInfoExists:Stack : ", se);
            }
         } catch (Exception e) {
            DEBUG.message("SAML2Utils:isFedInfoExists: Exception : ", e);
         }

         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils:isFedInfoExists : " + exists);
         }

         return exists;
      } else {
         return exists;
      }
   }

   public static Map<String, Set<String>> getNameIDKeyMap(NameID nameID, String hostEntityID, String remoteEntityID, String realm, String hostEntityRole) throws SAML2Exception {
      if (nameID == null) {
         throw new SAML2Exception(BUNDLE.getString("nullNameID"));
      }

      String affiliationID = nameID.getSPNameQualifier();
      NameIDInfoKey infoKey;
      if (affiliationID != null) {
         AffiliationDescriptorType affiDesc = META_MANAGER.getAffiliationDescriptor(realm, affiliationID);
         if (affiDesc == null) {
            infoKey = new NameIDInfoKey(nameID.getValue(), hostEntityID, remoteEntityID);
         } else if ("SPRole".equals(hostEntityRole)) {
            if (!affiDesc.getAffiliateMember().contains(hostEntityID)) {
               throw new SAML2Exception(BUNDLE.getString("spNotAffiliationMember"));
            }

            infoKey = new NameIDInfoKey(nameID.getValue(), affiliationID, remoteEntityID);
         } else {
            if (!affiDesc.getAffiliateMember().contains(remoteEntityID)) {
               throw new SAML2Exception(BUNDLE.getString("spNotAffiliationMember"));
            }

            infoKey = new NameIDInfoKey(nameID.getValue(), hostEntityID, affiliationID);
         }
      } else {
         infoKey = new NameIDInfoKey(nameID.getValue(), hostEntityID, remoteEntityID);
      }

      HashSet<String> set = new HashSet<>();
      set.add(infoKey.toValueString());
      Map<String, Set<String>> keyMap = new HashMap<>();
      keyMap.put(AccountUtils.getNameIDInfoKeyAttribute(), set);
      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Utils.getNameIDKeyMap: " + keyMap);
      }

      return keyMap;
   }

   public static boolean isSourceSiteValid(Issuer issuer, String orgName, String hostEntityId) {
      boolean isValid = false;

      try {
         if (issuer != null) {
            String entityID = issuer.getValue().trim();
            if (!entityID.isEmpty()) {
               isValid = META_MANAGER.isTrustedProvider(orgName, hostEntityId, entityID);
            } else {
               DEBUG.warning("Issuer from request is null");
            }
         }

         return isValid;
      } catch (Exception e) {
         DEBUG.error("SAML2Utils.isSourceSiteValid: Exception : ", e);
         return false;
      }
   }

   public static DataStoreProvider getDataStoreProvider() throws SAML2Exception {
      try {
         DataStoreProviderManager dsManager = DataStoreProviderManager.getInstance();
         return dsManager.getDataStoreProvider("saml2");
      } catch (DataStoreProviderException dse) {
         DEBUG.error("SAML2Utils.getDataStoreProvider: DataStoreProviderException : ", dse);
         throw new SAML2Exception(dse);
      }
   }

   public static String encodeForPOST(String str) {
      return Base64.encode(str.getBytes(StandardCharsets.UTF_8), 76);
   }

   public static String encodeForRedirect(String str) {
      String classMethod = "encodeForRedirect";
      int n = str.length();
      byte[] input = str.getBytes(StandardCharsets.UTF_8);
      byte[] output = new byte[n];
      Deflater deflater = new Deflater(-1, true);
      deflater.setInput(input);
      deflater.finish();
      int len = deflater.deflate(output);
      deflater.end();
      byte[] exact = new byte[len];
      System.arraycopy(output, 0, exact, 0, len);
      String base64Str = Base64.encode(exact);
      String encoded = URLEncDec.encode(base64Str);
      if (DEBUG.messageEnabled()) {
         DEBUG.message(classMethod + "out string length : " + encoded.length());
         DEBUG.message(classMethod + "out string is ===>" + encoded + "<===");
      }

      return encoded;
   }

   public static String decodeFromRedirect(String str) {
      String classMethod = "SAML2Utils.decodeFromRedirect: ";
      if (str != null && !str.isEmpty()) {
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils.decodeFromRedirect: input string length : " + str.length());
            DEBUG.message("SAML2Utils.decodeFromRedirect: input string is ===>" + str + "<===");
         }

         byte[] input = Base64.decode(removeNewLineChars(str));
         if (input != null && input.length != 0) {
            Inflater inflater = new Inflater(true);
            inflater.setInput(input);
            int resultLen = 2048;

            try {
               if (BUFFER_LEN != null && !BUFFER_LEN.isEmpty()) {
                  resultLen = Integer.parseInt(BUFFER_LEN);
               }
            } catch (NumberFormatException nfe) {
               DEBUG.error("Unable to parse buffer length.", nfe);
            }

            byte[] result = new byte[resultLen];
            int resultLength = 0;

            try {
               resultLength = inflater.inflate(result);
            } catch (DataFormatException dfe) {
               DEBUG.error("SAML2Utils.decodeFromRedirect: cannot inflate SAMLRequest: ", dfe);
               return null;
            }

            inflater.end();
            String outputString = new String(result, 0, resultLength, StandardCharsets.UTF_8);
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.decodeFromRedirect: Return value: \n" + outputString);
            }

            return outputString;
         } else {
            DEBUG.error("SAML2Utils.decodeFromRedirect: base 64 decoded result is null");
            return null;
         }
      } else {
         DEBUG.error("SAML2Utils.decodeFromRedirect: input is null.");
         return null;
      }
   }

   private static String removeNewLineChars(String s) {
      String retString;
      if (s != null && !s.isEmpty() && s.indexOf(10) != -1) {
         char[] chars = s.toCharArray();
         int len = chars.length;
         StringBuilder sb = new StringBuilder(len);

         for (char c : chars) {
            if (c != '\n') {
               sb.append(c);
            }
         }

         retString = sb.toString();
      } else {
         retString = s;
      }

      return retString;
   }

   public static SAML2MetaManager getSAML2MetaManager() {
      return META_MANAGER;
   }

   public static String getRealm(String realm) {
      return realm != null && !realm.isEmpty() ? realm : "/";
   }

   public static String getRealm(Map<String, String> paramsMap) {
      String realm = getParameter(paramsMap, "realm");
      return realm != null && !realm.isEmpty() ? realm : "/";
   }

   public static String getParameter(Map<String, String> paramsMap, String attrName) {
      String attrVal = null;
      if (paramsMap != null && !paramsMap.isEmpty()) {
         String attr = paramsMap.get(attrName);
         if (attr != null) {
            attrVal = attr;
         }
      }

      return attrVal;
   }

   public static String generateSourceID(String entityID) {
      if (entityID != null && !entityID.isEmpty()) {
         MessageDigest md;
         try {
            md = MessageDigest.getInstance("SHA");
         } catch (NoSuchAlgorithmException e) {
            DEBUG.error("SAML2Utils.generateSourceID: ", e);
            return null;
         }

         char[] chars = entityID.toCharArray();
         byte[] bytes = new byte[chars.length];

         for (int i = 0; i < chars.length; i++) {
            bytes[i] = (byte)chars[i];
         }

         md.update(bytes);
         return byteArrayToString(md.digest());
      } else {
         return null;
      }
   }

   public static String getRemoteServiceURL(String id) {
      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Utils.getRemoteServiceURL: id = " + id);
      }

      if (id != null && id.length() >= 2) {
         String serverID = id.substring(id.length() - 2);

         try {
            String localServerID = SystemConfigurationUtil.getServerID(SERVER_PROTOCOL, SERVER_HOST, SERVER_PORT, SERVER_URI);
            return serverID.equals(localServerID) ? null : SystemConfigurationUtil.getServerFromID(serverID);
         } catch (Exception ex) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.getRemoteServiceURL:", ex);
            }

            return null;
         }
      } else {
         return null;
      }
   }

   public static String generateIDWithServerID() {
      byte[] bytes = new byte[20];
      RANDOM.nextBytes(bytes);
      String id = "s2" + byteArrayToHexString(bytes);
      return embedServerID(id);
   }

   public static String generateMessageHandleWithServerID() {
      byte[] bytes = new byte[20];
      RANDOM.nextBytes(bytes);
      String id = byteArrayToString(bytes);
      return embedServerID(id);
   }

   private static String embedServerID(String id) {
      try {
         String serverId = SystemConfigurationUtil.getServerID(SERVER_PROTOCOL, SERVER_HOST, SERVER_PORT, SERVER_URI);
         if (serverId.length() == 2) {
            id = id.substring(0, id.length() - 2) + serverId;
         } else if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils.appendServerID: invalid server id = " + serverId);
         }
      } catch (Exception ex) {
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils.appendServerID:", ex);
         }
      }

      return id;
   }

   public static SOAPMessage createSOAPMessage(String xmlString, boolean isClientMessage) throws SOAPException, SAML2Exception {
      return createSOAPMessage(null, xmlString, isClientMessage);
   }

   public static SOAPMessage createSOAPMessage(String header, String body, boolean isClientMessage) throws SOAPException, SAML2Exception {
      try {
         MimeHeaders mimeHeaders = new MimeHeaders();
         mimeHeaders.addHeader("Content-Type", "text/xml");
         if (isClientMessage) {
            mimeHeaders.addHeader("Accept", "text/xml");
            mimeHeaders.addHeader("SOAPAction", "\"\"");
         }

         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils.createSOAPMessage: header = " + header + ", body = " + body);
         }

         StringBuilder sb = new StringBuilder(500);
         sb.append("<")
            .append("soap-env")
            .append(":Envelope")
            .append(" ")
            .append("xmlns:")
            .append("soap-env")
            .append("=\"")
            .append("http://schemas.xmlsoap.org/soap/envelope/")
            .append("\">");
         if (header != null) {
            sb.append("<").append("soap-env").append(":Header>").append(header).append("</").append("soap-env").append(":Header>");
         }

         if (body != null) {
            sb.append("<").append("soap-env").append(":Body>").append(body).append("</").append("soap-env").append(":Body>");
         }

         sb.append("</").append("soap-env").append(":Envelope>").append("\n");
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils.createSOAPMessage: soap message = " + sb);
         }

         return MESSAGE_FACTORY.createMessage(mimeHeaders, new ByteArrayInputStream(sb.toString().getBytes("UTF-8")));
      } catch (IOException io) {
         DEBUG.error("SAML2Utils.createSOAPMessage: IOE", io);
         throw new SAML2Exception(io.getMessage());
      }
   }

   public static String soapMessageToString(SOAPMessage message) {
      try {
         ByteArrayOutputStream bop = new ByteArrayOutputStream();
         message.writeTo(bop);
         return bop.toString(StandardCharsets.UTF_8);
      } catch (IOException | SOAPException ie) {
         DEBUG.error("SAML2Utils.soapMessageToString:", ie);
         return null;
      }
   }

   public static Element getSOAPBody(SOAPMessage message) throws SAML2Exception {
      DEBUG.message("SAML2Utils.getSOAPBody : start");
      ByteArrayOutputStream bop = new ByteArrayOutputStream();

      try {
         message.writeTo(bop);
      } catch (IOException ie) {
         DEBUG.error("SAML2Utils.getSOAPBody : writeTo IO", ie);
         throw new SAML2Exception(ie.getMessage());
      } catch (SOAPException se) {
         DEBUG.error("SAML2Utils.getSOAPBody : writeTo SOAP", se);
         throw new SAML2Exception(se.getMessage());
      }

      ByteArrayInputStream bin = new ByteArrayInputStream(bop.toByteArray());
      Document doc = XMLUtils.toDOMDocument(bin, DEBUG);
      Element root = doc.getDocumentElement();
      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Util.getSOAPBody : soap body =\n" + XMLUtils.print(root));
      }

      String rootName = doc.getDocumentElement().getLocalName();
      if (rootName != null && !rootName.isEmpty()) {
         if (rootName.equals("Envelope") && "http://schemas.xmlsoap.org/soap/envelope/".equals(root.getNamespaceURI())) {
            NodeList nl = root.getChildNodes();
            int length = nl.getLength();
            if (length <= 0) {
               DEBUG.error("SAML2Utils.getSOAPBody: no msg body");
               throw new SAML2Exception(BUNDLE.getString("missingSOAPBody"));
            }

            for (int i = 0; i < length; i++) {
               Node child = nl.item(i);
               if (child.getNodeType() != 1) {
                  DEBUG.message("SAML2Utils.getSOAPBody: " + child);
               } else {
                  String childName = child.getLocalName();
                  if (DEBUG.messageEnabled()) {
                     DEBUG.message("SAML2Utils.getSOAPBody: local name= " + childName);
                  }

                  if (childName.equals("Body") && "http://schemas.xmlsoap.org/soap/envelope/".equals(child.getNamespaceURI())) {
                     return (Element)child;
                  }
               }
            }

            throw new SAML2Exception(BUNDLE.getString("missingSOAPBody"));
         } else {
            DEBUG.error("SAML2Utils.getSOAPBody : either root element is not Envelope or invalid name space or prefix");
            throw new SAML2Exception(BUNDLE.getString("invalidSOAPElement"));
         }
      } else {
         DEBUG.error("SAML2Utils.getSOAPBody : no local name");
         throw new SAML2Exception(BUNDLE.getString("missingLocalName"));
      }
   }

   public static MimeHeaders getHeaders(HttpServletRequest req) {
      Enumeration<String> e = req.getHeaderNames();
      MimeHeaders headers = new MimeHeaders();

      while (e.hasMoreElements()) {
         String headerName = e.nextElement();
         String headerValue = req.getHeader(headerName);
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Util.getHeaders: Header name=" + headerName + ", value=" + headerValue);
         }

         StringTokenizer values = new StringTokenizer(headerValue, ",");

         while (values.hasMoreTokens()) {
            headers.addHeader(headerName, values.nextToken().trim());
         }
      }

      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Util.getHeaders: Header=" + headers);
      }

      return headers;
   }

   public static void putHeaders(MimeHeaders headers, HttpServletResponse res) {
      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Util.putHeaders: Header=" + headers.toString());
      }

      Iterator<MimeHeader> it = headers.getAllHeaders();

      while (it.hasNext()) {
         MimeHeader header = it.next();
         if (!header.getName().equalsIgnoreCase("Content-Length")) {
            String[] values = headers.getHeader(header.getName());
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Util.putHeaders: Header name=" + header.getName() + ", value=" + Arrays.toString(values));
            }

            if (values.length == 1) {
               res.setHeader(header.getName(), header.getValue());
            } else {
               StringBuilder concat = new StringBuilder();

               for (int i = 0; i < values.length; concat.append(values[i++])) {
                  if (i != 0) {
                     concat.append(',');
                  }
               }

               res.setHeader(header.getName(), concat.toString());
            }
         }
      }
   }

   public static Status generateStatus(String code, String message) {
      return generateStatus(code, null, message);
   }

   public static Status generateStatus(String code, String subCode, String message) {
      Status status = null;

      try {
         status = ProtocolFactory.getInstance().createStatus();
         StatusCode statusCode = ProtocolFactory.getInstance().createStatusCode();
         statusCode.setValue(code);
         status.setStatusCode(statusCode);
         if (message != null && !message.isEmpty()) {
            status.setStatusMessage(message);
         }

         if (subCode != null) {
            StatusCode subStatusCode = ProtocolFactory.getInstance().createStatusCode();
            subStatusCode.setValue(subCode);
            statusCode.setStatusCode(subStatusCode);
         }
      } catch (SAML2Exception e) {
         DEBUG.error("SAML2Utils.generateStatus:", e);
      }

      return status;
   }

   public static Response getErrorResponse(RequestAbstract request, String code, String subCode, String statusMsg, String issuerEntityID) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getErrorResponse: ";
      Response errResp = ProtocolFactory.getInstance().createResponse();
      errResp.setStatus(generateStatus(code, subCode, statusMsg));
      String responseID = generateID();
      if (responseID == null) {
         DEBUG.error("Unable to generate response ID.");
         return null;
      }

      errResp.setID(responseID);
      if (request != null) {
         errResp.setInResponseTo(request.getID());
      }

      errResp.setVersion("2.0");
      errResp.setIssueInstant(new Date());
      if (issuerEntityID != null) {
         Issuer issuer = AssertionFactory.getInstance().createIssuer();
         issuer.setValue(issuerEntityID);
         errResp.setIssuer(issuer);
      }

      if (DEBUG.messageEnabled()) {
         DEBUG.message(classMethod + "Error Response is : " + errResp.toXMLString());
      }

      return errResp;
   }

   public static Element getSamlpElement(SOAPMessage message, String localName) throws SAML2Exception {
      Element body = getSOAPBody(message);
      NodeList nlBody = body.getChildNodes();
      int blength = nlBody.getLength();
      if (blength <= 0) {
         DEBUG.error("SAML2Utils.getSamlpElement: empty body");
         throw new SAML2Exception(BUNDLE.getString("missingBody"));
      }

      Element retElem = null;
      Node node = null;

      for (int i = 0; i < blength; i++) {
         node = nlBody.item(i);
         if (node.getNodeType() == 1) {
            String nlName = node.getLocalName();
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.getSamlpElement: node=" + nlName + ", nsURI=" + node.getNamespaceURI());
            }

            if (nlName.equals("Fault")) {
               throw new SAML2Exception(BUNDLE.getString("soapFaultInSOAPResponse"));
            }

            if (nlName.equals(localName) && "urn:oasis:names:tc:SAML:2.0:protocol".equals(node.getNamespaceURI())) {
               retElem = (Element)node;
               break;
            }
         }
      }

      if (retElem == null) {
         throw new SAML2Exception(BUNDLE.getString("elementNotFound") + localName);
      } else {
         return retElem;
      }
   }

   public static SOAPMessage createSOAPFault(String faultCode, String faultString, String detail) {
      SOAPMessage msg = null;
      SOAPEnvelope envelope = null;
      SOAPFault sf = null;
      SOAPBody body = null;
      SOAPElement se = null;

      try {
         msg = MESSAGE_FACTORY.createMessage();
         envelope = msg.getSOAPPart().getEnvelope();
         body = envelope.getBody();
         sf = body.addFault();
         Name qName = envelope.createName(faultCode, null, "http://schemas.xmlsoap.org/soap/envelope/");
         sf.setFaultCode(qName);
         sf.setFaultString(BUNDLE.getString(faultString));
         if (detail != null && !detail.isEmpty()) {
            Detail det = sf.addDetail();
            SOAPElement var14 = det.addDetailEntry(envelope.createName("Problem"));
            var14.addAttribute(envelope.createName("details"), BUNDLE.getString(detail));
         }
      } catch (SOAPException e) {
         DEBUG.error("createSOAPFault:", e);
      }

      return msg;
   }

   public static SOAPMessage sendSOAPMessage(String xmlMessage, String soapUrl, boolean isClientMessage) throws SOAPException, SAML2Exception {
      SOAPConnection con = CONNECTION_FACTORY.createConnection();
      SOAPMessage msg = createSOAPMessage(xmlMessage, isClientMessage);
      return con.call(msg, soapUrl);
   }

   public static String getEncryptionCertAlias(String realm, String hostEntityId, String entityRole) {
      if (DEBUG.messageEnabled()) {
         String method = "getEncryptionCertAlias : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
      }

      return getAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, "encryptionCertAlias");
   }

   public static String getSigningCertAlias(String realm, String hostEntityId, String entityRole) {
      if (DEBUG.messageEnabled()) {
         String method = "getSigningCertAlias : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
      }

      return getAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, "signingCertAlias");
   }

   public static boolean getWantNameIDEncrypted(String realm, String hostEntityId, String entityRole) {
      if (DEBUG.messageEnabled()) {
         String method = "getWantNameIDEncrypted : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
      }

      String wantEncrypted = getAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, "wantNameIDEncrypted");
      if (wantEncrypted == null) {
         wantEncrypted = "false";
      }

      return wantEncrypted.equalsIgnoreCase("true");
   }

   public static boolean getWantLogoutRequestSigned(String realm, String hostEntityId, String entityRole) {
      if (DEBUG.messageEnabled()) {
         String method = "getWantLogoutRequestSigned : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
      }

      String wantSigned = getAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, "wantLogoutRequestSigned");
      if (wantSigned == null) {
         wantSigned = "false";
      }

      return wantSigned.equalsIgnoreCase("true");
   }

   public static boolean getWantLogoutResponseSigned(String realm, String hostEntityId, String entityRole) {
      if (DEBUG.messageEnabled()) {
         String method = "getWantLogoutResponseSigned : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
      }

      String wantSigned = getAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, "wantLogoutResponseSigned");
      if (wantSigned == null) {
         wantSigned = "false";
      }

      return wantSigned.equalsIgnoreCase("true");
   }

   public static String getAttributeValueFromSSOConfig(String realm, String hostEntityId, String entityRole, String attrName) {
      if (DEBUG.messageEnabled()) {
         String method = "getAttributeValueFromSSOConfig : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
         DEBUG.message(method + "attrName - " + attrName);
      }

      List<String> value = getAllAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, attrName);
      if (DEBUG.messageEnabled()) {
         DEBUG.message("getAttributeValueFromSSOConfig: values=" + value);
      }

      return value != null && !value.isEmpty() ? value.getFirst() : null;
   }

   public static List<String> getAllAttributeValueFromSSOConfig(String realm, String hostEntityId, String entityRole, String attrName) {
      if (DEBUG.messageEnabled()) {
         String method = "getAllAttributeValueFromSSOConfig : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
         DEBUG.message(method + "attrName - " + attrName);
      }

      try {
         BaseConfigType config = null;
         if (entityRole.equalsIgnoreCase("SPRole")) {
            config = META_MANAGER.getSPSSOConfig(realm, hostEntityId);
         } else if (entityRole.equalsIgnoreCase("IDPRole")) {
            config = META_MANAGER.getIDPSSOConfig(realm, hostEntityId);
         }

         if (config == null) {
            return null;
         }

         Map<String, List<String>> attrs = SAML2MetaUtils.getAttributes(config);
         return attrs.get(attrName);
      } catch (SAML2MetaException e) {
         DEBUG.message("get SSOConfig failed:", e);
         return null;
      }
   }

   public static String getHostEntityRole(Map<String, String> paramsMap) throws SAML2Exception {
      String roleName = getParameter(paramsMap, "role");
      if (!roleName.equalsIgnoreCase("SPRole") && !roleName.equalsIgnoreCase("IDPRole")) {
         throw new SAML2Exception(BUNDLE.getString("unknownHostEntityRole"));
      } else {
         return roleName;
      }
   }

   public static boolean isDualRole(String hostEntityId, String realm) {
      try {
         BaseConfigType spConfig = META_MANAGER.getSPSSOConfig(realm, hostEntityId);
         if (spConfig == null) {
            return false;
         }

         BaseConfigType idpConfig = META_MANAGER.getIDPSSOConfig(realm, hostEntityId);
         return idpConfig != null;
      } catch (Exception e) {
         return false;
      }
   }

   public static Issuer createIssuer(String entityID) throws SAML2Exception {
      String method = "createIssuer: ";
      Issuer issuer = ASSERTION_FACTORY.createIssuer();
      issuer.setValue(entityID);
      if (DEBUG.messageEnabled()) {
         DEBUG.message(method + "Issuer : " + issuer.toXMLString());
      }

      return issuer;
   }

   public static String signQueryString(String queryString, String realm, String hostEntity, String hostEntityRole) throws SAML2Exception {
      String method = "signQueryString : ";
      if (DEBUG.messageEnabled()) {
         DEBUG.message(method + "queryString :" + queryString);
      }

      String alias = getSigningCertAlias(realm, hostEntity, hostEntityRole);
      if (DEBUG.messageEnabled()) {
         DEBUG.message(method + "realm is : " + realm);
         DEBUG.message(method + "hostEntity is : " + hostEntity);
         DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
         DEBUG.message(method + "Signing Cert Alias is : " + alias);
      }

      PrivateKey signingKey = KEY_PROVIDER.getPrivateKey(alias);
      if (signingKey == null) {
         DEBUG.error("Incorrect configuration for Signing Certificate.");
         throw new SAML2Exception(BUNDLE.getString("metaDataError"));
      } else {
         return QuerySignatureUtil.sign(queryString, signingKey);
      }
   }

   public static boolean verifyQueryString(String queryString, String realm, String hostEntityRole, String remoteEntity) throws SAML2Exception {
      String method = "verifyQueryString : ";
      if (DEBUG.messageEnabled()) {
         DEBUG.message(method + "queryString :" + queryString);
      }

      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 (DEBUG.messageEnabled()) {
         DEBUG.message(method + "realm is : " + realm);
         DEBUG.message(method + "Host Entity role is : " + hostEntityRole);
         DEBUG.message(method + "remoteEntity is : " + remoteEntity);
      }

      if (signingCerts.isEmpty()) {
         DEBUG.error("Incorrect configuration for Signing Certificate.");
         throw new SAML2Exception(BUNDLE.getString("metaDataError"));
      } else {
         return QuerySignatureUtil.verify(queryString, signingCerts);
      }
   }

   public static String createNameIdentifier() {
      String handle = null;

      try {
         byte[] handleBytes = new byte[21];
         RANDOM.nextBytes(handleBytes);
         handle = Base64.encode(handleBytes);
         if (DEBUG.messageEnabled()) {
            DEBUG.message("createNameIdentifier String: " + handle);
         }
      } catch (Exception e) {
         DEBUG.message("createNameIdentifier: Exception during proccessing request" + e.getMessage());
      }

      return handle;
   }

   public static SPAuthnContextMapper getSPAuthnContextMapper(String realm, String hostEntityID, String authnCtxClassName) {
      SPAuthnContextMapper spAuthnCtx = SPCache.authCtxObjHash.get(hostEntityID + "|" + realm);
      if (DEBUG.messageEnabled()) {
         DEBUG.message("AuthContext Class Name is :" + authnCtxClassName);
      }

      if (spAuthnCtx == null && authnCtxClassName != null && !authnCtxClassName.isEmpty()) {
         try {
            spAuthnCtx = (SPAuthnContextMapper)Class.forName(authnCtxClassName).getDeclaredConstructor().newInstance();
            SPCache.authCtxObjHash.put(hostEntityID + "|" + realm, spAuthnCtx);
         } catch (ClassNotFoundException ce) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils: Mapper not configured using Default AuthnContext Mapper");
            }
         } catch (InstantiationException ie) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils: Instantiation ");
               DEBUG.message("SAML2Utils:Error instantiating : using Default AuthnContext Mapper");
            }
         } catch (IllegalAccessException iae) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils: illegalaccess");
               DEBUG.message("SAML2Utils:Error : using Default AuthnContext Mapper");
            }
         } catch (Exception e) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils:Error : using Default AuthnContext Mapper");
            }
         }
      }

      if (spAuthnCtx == null) {
         spAuthnCtx = new DefaultSPAuthnContextMapper();
         SPCache.authCtxObjHash.put(hostEntityID + "|" + realm, spAuthnCtx);
      }

      return spAuthnCtx;
   }

   public static void verifyRequestIssuer(String realm, String hostEntity, Issuer reqIssuer, String requestId) throws SAML2Exception {
      boolean issuerValid = isSourceSiteValid(reqIssuer, realm, hostEntity);
      if (!issuerValid) {
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils Issuer in Request is not valid.");
         }

         String[] data = new String[]{hostEntity, realm, requestId};
         LogUtil.error(Level.INFO, "INVALID_ISSUER_REQUEST", data, null);
         throw new SAML2Exception(BUNDLE.getString("invalidIssuerInRequest"));
      }
   }

   public static void verifyResponseIssuer(String realm, String hostEntity, Issuer resIssuer, String requestId) throws SAML2Exception {
      boolean issuerValid = isSourceSiteValid(resIssuer, realm, hostEntity);
      if (!issuerValid) {
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils Issuer in Response is not valid.");
         }

         String[] data = new String[]{hostEntity, realm, requestId};
         LogUtil.error(Level.INFO, "INVALID_ISSUER_RESPONSE", data, null);
         throw new SAML2Exception(BUNDLE.getString("invalidIssuerInResponse"));
      }
   }

   public static IDPAccountMapper getIDPAccountMapper(String realm, String idpEntityID) throws SAML2Exception {
      String classMethod = "SAML2Utils.getIDPAccountMapper: ";
      String idpAccountMapperName = null;
      IDPAccountMapper idpAccountMapper = null;

      try {
         idpAccountMapperName = getAttributeValueFromSSOConfig(realm, idpEntityID, "IDPRole", "idpAccountMapper");
         if (idpAccountMapperName == null) {
            idpAccountMapperName = SAML2Constants.DEFAULT_IDP_ACCOUNT_MAPPER_CLASS;
            if (DEBUG.messageEnabled()) {
               DEBUG.message(classMethod + "use " + SAML2Constants.DEFAULT_IDP_ACCOUNT_MAPPER_CLASS);
            }
         }

         idpAccountMapper = IDPCache.idpAccountMapperCache.get(idpAccountMapperName);
         if (idpAccountMapper == null) {
            idpAccountMapper = (IDPAccountMapper)Class.forName(idpAccountMapperName).getDeclaredConstructor().newInstance();
            IDPCache.idpAccountMapperCache.put(idpAccountMapperName, idpAccountMapper);
         } else if (DEBUG.messageEnabled()) {
            DEBUG.message(classMethod + "got the IDPAccountMapper from cache");
         }

         return idpAccountMapper;
      } catch (Exception ex) {
         DEBUG.error(classMethod + "Unable to get IDP Account Mapper.", ex);
         throw new SAML2Exception(ex);
      }
   }

   public static SAML2ServiceProviderAdapter getSPAdapterClass(String spEntityID, String realm) throws SAML2Exception {
      String classMethod = "SAML2Utils.getSPAdapterClass: ";
      if (DEBUG.messageEnabled()) {
         DEBUG.message(classMethod + "get SPAdapter for " + spEntityID + " under realm " + realm);
      }

      String spAdapterClassName = null;
      SAML2ServiceProviderAdapter spAdapterClass = null;

      try {
         spAdapterClassName = getAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "spAdapter");
         if (DEBUG.messageEnabled()) {
            DEBUG.message(classMethod + "get SPAdapter class " + spAdapterClassName);
         }

         if (spAdapterClassName != null && !spAdapterClassName.isEmpty()) {
            spAdapterClass = SPCache.spAdapterClassCache.get(realm + spEntityID + spAdapterClassName);
            if (spAdapterClass == null) {
               spAdapterClass = (SAML2ServiceProviderAdapter)Class.forName(spAdapterClassName).getDeclaredConstructor().newInstance();
               List<String> env = getAllAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "spAdapterEnv");
               Map<String, String> map = parseEnvList(env);
               map.put("HOSTED_ENTITY_ID", spEntityID);
               map.put("REALM", realm);
               spAdapterClass.initialize(map);
               SPCache.spAdapterClassCache.put(realm + spEntityID + spAdapterClassName, spAdapterClass);
               if (DEBUG.messageEnabled()) {
                  DEBUG.message(classMethod + "create new SPAdapter " + spAdapterClassName + " for " + spEntityID + " under realm " + realm);
               }
            } else if (DEBUG.messageEnabled()) {
               DEBUG.message(classMethod + "got the SPAdapter " + spAdapterClassName + " from cache");
            }
         }

         return spAdapterClass;
      } catch (InstantiationException | InvocationTargetException | NoSuchMethodException ex) {
         DEBUG.error(classMethod + "Unable to get SP Adapter class instance.", ex);
         throw new SAML2Exception(ex);
      } catch (ClassNotFoundException ex) {
         DEBUG.error(classMethod + "SP Adapter class not found.", ex);
         throw new SAML2Exception(ex);
      } catch (IllegalAccessException ex) {
         DEBUG.error(classMethod + "Unable to get SP Adapter class.", ex);
         throw new SAML2Exception(ex);
      }
   }

   private static Map<String, String> parseEnvList(List<String> list) {
      Map<String, String> map = new HashMap<>();
      if (list != null && !list.isEmpty()) {
         for (String val : list) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.parseEnvList : processing " + val);
            }

            if (val != null && !val.isEmpty()) {
               int pos = val.indexOf("=");
               if (pos == -1) {
                  if (DEBUG.warningEnabled()) {
                     DEBUG.warning("SAML2Utils.parseEnvList : invalid value : " + val + ". Value must be in key=value format.");
                  }
               } else {
                  map.put(val.substring(0, pos), val.substring(pos + 1));
               }
            }
         }

         return map;
      } else {
         return map;
      }
   }

   public static String getRelayState(HttpServletRequest request) {
      String relayState = request.getParameter("RelayState");
      if (relayState == null || relayState.isEmpty()) {
         String relayStateAlias = request.getParameter("RelayStateAlias");
         if (relayStateAlias != null && !relayStateAlias.isEmpty()) {
            StringTokenizer st = new StringTokenizer(relayStateAlias, "|");

            while (st.hasMoreTokens()) {
               String tmp = st.nextToken();
               relayState = request.getParameter(tmp);
               if (relayState != null && !relayState.isEmpty()) {
                  break;
               }
            }
         }

         if (relayState == null) {
            relayState = request.getParameter("goto");
         }
      }

      return relayState;
   }

   public static boolean verifyDestination(String destination, String location) {
      return location != null && !location.isEmpty() && destination != null && !destination.isEmpty() && location.equalsIgnoreCase(destination);
   }

   public static String getNameIDStringFromResponse(Response response) {
      if (response != null) {
         List<Assertion> assertions = response.getAssertion();
         if (assertions != null && !assertions.isEmpty()) {
            Assertion assertion = assertions.getFirst();
            Subject subject = assertion.getSubject();
            if (subject != null) {
               NameID nameID = subject.getNameID();
               if (nameID != null) {
                  return nameID.getValue();
               }
            }
         }
      }

      return null;
   }

   public static Map<String, String> getConfigAttributeMap(String realm, String hostEntityID, String role) throws SAML2Exception {
      if (realm == null) {
         throw new SAML2Exception(BUNDLE.getString("nullRealm"));
      }

      if (hostEntityID == null) {
         throw new SAML2Exception(BUNDLE.getString("nullHostEntityID"));
      }

      if (DEBUG.messageEnabled()) {
         DEBUG.message("SAML2Utils.getConfigAttributeMap: DefaultAttrMapper: realm=" + realm + ", entity id=" + hostEntityID + ", role=" + role);
      }

      try {
         BaseConfigType config = null;
         if (role.equals("SPRole")) {
            config = META_MANAGER.getSPSSOConfig(realm, hostEntityID);
         } else if (role.equals("IDPRole")) {
            config = META_MANAGER.getIDPSSOConfig(realm, hostEntityID);
         }

         if (config == null) {
            if (DEBUG.warningEnabled()) {
               DEBUG.warning("SAML2Utils.getConfigAttributeMap: configuration is not defined.");
            }

            return Collections.emptyMap();
         } else {
            Map<String, List<String>> attribConfig = SAML2MetaUtils.getAttributes(config);
            List<String> mappedAttributes = attribConfig.get("attributeMap");
            if (mappedAttributes != null && !mappedAttributes.isEmpty()) {
               Map<String, String> map = new HashMap<>();

               for (String entry : mappedAttributes) {
                  if (!entry.contains("=")) {
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message("SAML2Utils.getConfigAttributeMap: Invalid entry." + entry);
                     }
                  } else {
                     StringTokenizer st = new StringTokenizer(entry, "=");
                     map.put(st.nextToken(), st.nextToken());
                  }
               }

               return map;
            } else {
               if (DEBUG.messageEnabled()) {
                  DEBUG.message("SAML2Utils.getConfigAttributeMap: Attribute map is not defined for entity: " + hostEntityID);
               }

               return Collections.emptyMap();
            }
         }
      } catch (SAML2MetaException sme) {
         DEBUG.error("SAML2Utils.getConfigAttributeMap: ", sme);
         throw new SAML2Exception(sme.getMessage());
      }
   }

   public static Attribute getSAMLAttribute(String name, String[] values) throws SAML2Exception {
      if (name == null) {
         throw new SAML2Exception(BUNDLE.getString("nullInput"));
      }

      AssertionFactory factory = AssertionFactory.getInstance();
      Attribute attribute = factory.createAttribute();
      attribute.setName(name);
      attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:basic");
      if (values != null) {
         List<String> list = new ArrayList<>();
         Collections.addAll(list, values);
         attribute.setAttributeValueString(list);
      }

      return attribute;
   }

   public static void postToTarget(
      HttpServletRequest request,
      HttpServletResponse response,
      String samlMessageName,
      String samlMessageValue,
      String relayStateName,
      String relayStateValue,
      String targetURL
   ) throws IOException {
      response.setHeader("Pragma", "no-cache");
      response.setHeader("Cache-Control", "no-cache,no-store");
      response.setContentType("text/html; charset=UTF-8");
      StringBuilder sb = new StringBuilder()
         .append("<HTML>")
         .append("<HEAD>\n")
         .append("<TITLE>Access rights validated</TITLE>\n")
         .append("</HEAD>\n")
         .append("<BODY>")
         .append("<FORM METHOD=\"POST\" ACTION=\"" + targetURL + "\">")
         .append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + samlMessageName + "\" VALUE=\"" + samlMessageValue + "\">\n");
      if (relayStateValue != null && !relayStateValue.isEmpty()) {
         sb.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + relayStateName + "\" VALUE=\"" + relayStateValue + "\">\n");
      }

      sb.append("<NOSCRIPT><CENTER>")
         .append("<INPUT TYPE=\"SUBMIT\" VALUE=\"" + BUNDLE.getString("samlPostKey") + "\"/></CENTER></NOSCRIPT>")
         .append("</FORM>")
         .append("<SCRIPT");
      String scriptNonce = scriptNonce(request);
      if (scriptNonce != null) {
         sb.append(" nonce=\"" + scriptNonce + "\"");
      }

      sb.append(" type=\"text/javascript\">").append("document.forms[0].submit();").append("</SCRIPT>").append("</BODY></HTML>");
      Saml2ResponseBodyWriter.writeBodyWithoutCommittingResponse(sb.toString(), response);
   }

   private static String scriptNonce(HttpServletRequest request) {
      Object attribute = request.getAttribute("cspNonce");
      return attribute instanceof String ? (String)attribute : null;
   }

   public static String verifyNameIDFormat(String nameIDFormat, SPSSODescriptorType spsso, IDPSSODescriptorType idpsso) throws SAML2Exception {
      List<String> spNameIDFormatList = spsso.getNameIDFormat();
      List<String> idpNameIDFormatList = idpsso == null ? null : idpsso.getNameIDFormat();
      if (nameIDFormat != null && !nameIDFormat.isEmpty()) {
         if (nameIDFormat.equals("persistent") || nameIDFormat.equals("transient")) {
            nameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:" + nameIDFormat;
         }

         if (spNameIDFormatList != null && !spNameIDFormatList.isEmpty() && !spNameIDFormatList.contains(nameIDFormat)) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.verifyNameIDFormat: NameIDFormat not supported by SP: " + nameIDFormat);
            }

            Object[] args = new Object[]{nameIDFormat};
            throw new SAML2Exception("libSAML2", "unsupportedNameIDFormatSP", args);
         }

         if (idpNameIDFormatList != null && !idpNameIDFormatList.isEmpty() && !idpNameIDFormatList.contains(nameIDFormat)) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.verifyNameIDFormat: NameIDFormat not supported by IDP: " + nameIDFormat);
            }

            Object[] args = new Object[]{nameIDFormat};
            throw new SAML2Exception("libSAML2", "unsupportedNameIDFormatIDP", args);
         }
      } else {
         if (idpNameIDFormatList == null || idpNameIDFormatList.isEmpty()) {
            if (spNameIDFormatList != null && !spNameIDFormatList.isEmpty()) {
               return spNameIDFormatList.getFirst();
            }

            return "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent";
         }

         if (spNameIDFormatList == null || spNameIDFormatList.isEmpty()) {
            return idpNameIDFormatList.getFirst();
         }

         nameIDFormat = null;

         for (String spNameIDFormat : spNameIDFormatList) {
            if (idpNameIDFormatList.contains(spNameIDFormat)) {
               nameIDFormat = spNameIDFormat;
               break;
            }
         }

         if (nameIDFormat == null) {
            throw new SAML2Exception(BUNDLE.getString("unsupportedNameIDFormatIDPSP"));
         }
      }

      return nameIDFormat;
   }

   public static boolean isAuthnContextMatching(List<String> requestedACClassRefs, String acClassRef, String comparison, Map<String, Object> acClassRefLevelMap) {
      Integer levelInt = (Integer)acClassRefLevelMap.get(acClassRef);
      if (levelInt == null) {
         if (DEBUG.messageEnabled()) {
            DEBUG.message("SAML2Utils.isAuthnContextMatching: AuthnContextClassRef " + acClassRef + " is not supported.");
         }

         return false;
      } else {
         int level = levelInt;
         if (comparison != null && !comparison.isEmpty() && !comparison.equals("exact")) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message("SAML2Utils.isAuthnContextMatching: acClassRef = " + acClassRef + ", level = " + level + ", comparison = " + comparison);
            }
            return switch (comparison) {
               case "minimum" -> {
                  for (String requstedACClassRef : requestedACClassRefs) {
                     Integer requestedLevelInt = (Integer)acClassRefLevelMap.get(requstedACClassRef);
                     int requestedLevel = requestedLevelInt == null ? 0 : requestedLevelInt;
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message("SAML2Utils.isAuthnContextMatching: requestedACClassRef = " + requstedACClassRef + ", level = " + requestedLevel);
                     }

                     if (level >= requestedLevel) {
                        yield true;
                     }
                  }

                  yield false;
               }
               case "better" -> {
                  for (String requstedACClassRef : requestedACClassRefs) {
                     Integer requestedLevelInt = (Integer)acClassRefLevelMap.get(requstedACClassRef);
                     int requestedLevel = requestedLevelInt == null ? 0 : requestedLevelInt;
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message("SAML2Utils.isAuthnContextMatching: requestedACClassRef = " + requstedACClassRef + ", level = " + requestedLevel);
                     }

                     if (level <= requestedLevel) {
                        yield false;
                     }
                  }

                  yield true;
               }
               case "maximum" -> {
                  for (String requstedACClassRef : requestedACClassRefs) {
                     Integer requestedLevelInt = (Integer)acClassRefLevelMap.get(requstedACClassRef);
                     int requestedLevel = requestedLevelInt == null ? 0 : requestedLevelInt;
                     if (DEBUG.messageEnabled()) {
                        DEBUG.message("SAML2Utils.isAuthnContextMatching: requestedACClassRef = " + requstedACClassRef + ", level = " + requestedLevel);
                     }

                     if (level <= requestedLevel) {
                        yield true;
                     }
                  }

                  yield false;
               }
               default -> false;
            };
         } else {
            for (String requstedACClassRef : requestedACClassRefs) {
               if (requstedACClassRef.equals(acClassRef)) {
                  return true;
               }
            }

            return false;
         }
      }
   }

   public static void postToAppLogout(HttpServletRequest request, String appLogoutURL, Object session) {
      String method = "SAML2Utils.postToAppLogout: ";

      try {
         if (appLogoutURL == null || appLogoutURL.isEmpty()) {
            return;
         }

         String logoutURL = appLogoutURL;
         String sessProp = null;
         int pos = appLogoutURL.indexOf("appsessionproperty=");
         if (pos != -1) {
            int endPos = appLogoutURL.indexOf("&", pos);
            if (endPos != -1) {
               sessProp = appLogoutURL.substring(pos + "appsessionproperty".length() + 1, endPos);
               logoutURL = appLogoutURL.substring(0, pos) + appLogoutURL.substring(endPos + 1);
            } else {
               sessProp = appLogoutURL.substring(pos + "appsessionproperty".length() + 1);
               logoutURL = appLogoutURL.substring(0, pos - 1);
            }
         }

         if (DEBUG.messageEnabled()) {
            DEBUG.message(method + "appLogoutURL=" + appLogoutURL + ", real logoutURL=" + logoutURL + ", session property name: " + sessProp);
         }

         URL url = URI.create(logoutURL).toURL();
         HttpURLConnection conn = HttpURLConnectionManager.getConnection(url);
         conn.setDoOutput(true);
         conn.setRequestMethod("POST");
         HttpURLConnection.setFollowRedirects(false);
         conn.setInstanceFollowRedirects(false);
         String strCookies = getCookiesString(request);
         if (strCookies != null) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message(method + "Sending cookies : " + strCookies);
            }

            conn.setRequestProperty("Cookie", strCookies);
         }

         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
         StringBuilder buffer = new StringBuilder();
         if (sessProp != null && session != null) {
            String[] values = SessionManager.getProvider().getProperty(session, sessProp);
            if (values != null && values.length != 0) {
               int i = 0;

               while (true) {
                  conn.setRequestProperty(URLEncDec.encode(sessProp), URLEncDec.encode(values[i]));
                  buffer.append(URLEncDec.encode(sessProp)).append('=');
                  buffer.append(URLEncDec.encode(values[i++]));
                  if (i == values.length) {
                     break;
                  }

                  buffer.append('&');
               }
            }
         }

         if (DEBUG.messageEnabled()) {
            DEBUG.message(method + "Sending content: " + buffer);
         }

         OutputStream outputStream = conn.getOutputStream();
         outputStream.write(buffer.toString().getBytes(StandardCharsets.UTF_8));
         outputStream.flush();
         outputStream.close();
         if (conn.getResponseCode() == 200) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message(method + "Response code OK");
            }
         } else {
            DEBUG.error(method + "Response code NOT OK: " + conn.getResponseCode());
         }
      } catch (SessionException | IOException ex) {
         DEBUG.error(method + " post to external app failed.", ex);
      }
   }

   private static String getCookiesString(HttpServletRequest request) {
      String method = "SAML2Utils.getCookiesString: ";
      Cookie[] cookies = request.getCookies();
      StringBuilder cookieStr = null;
      String strCookies = null;
      if (cookies != null) {
         for (Cookie cookie : cookies) {
            if (DEBUG.messageEnabled()) {
               DEBUG.message(method + "Cookie name = " + cookie.getName());
               DEBUG.message(method + " Cookie value = " + cookie.getValue());
            }

            if (cookieStr == null) {
               cookieStr = new StringBuilder();
            } else {
               cookieStr.append(";");
            }

            cookieStr.append(cookie.getName()).append("=").append(cookie.getValue());
         }
      }

      if (cookieStr != null) {
         strCookies = cookieStr.toString();
      }

      return strCookies;
   }

   public static boolean wantPOSTResponseSigned(String realm, String hostEntityId, String entityRole) {
      if (DEBUG.messageEnabled()) {
         String method = "SAML2Utils:getWantPOSTResponseSigned : ";
         DEBUG.message(method + "realm - " + realm);
         DEBUG.message(method + "hostEntityId - " + hostEntityId);
         DEBUG.message(method + "entityRole - " + entityRole);
      }

      String wantSigned = getAttributeValueFromSSOConfig(realm, hostEntityId, entityRole, "wantPOSTResponseSigned");
      return "true".equalsIgnoreCase(wantSigned);
   }
}
