package com.sun.identity.saml2.profile;

import com.airlock.iam.openfedlib.saml2.XjcHelper;
import com.airlock.iam.openfedlib.saml2.servlet.Saml2ResponseBodyWriter;
import com.airlock.iam.openfedlib.saml2.state.Saml2DataMapProvider;
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.plugin.session.SessionException;
import com.sun.identity.plugin.session.SessionManager;
import com.sun.identity.plugin.session.SessionProvider;
import com.sun.identity.saml.common.SAMLUtils;
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.AttributeStatement;
import com.sun.identity.saml2.assertion.AudienceRestriction;
import com.sun.identity.saml2.assertion.AuthnContext;
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.EncryptedAttribute;
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.assertion.Subject;
import com.sun.identity.saml2.assertion.SubjectConfirmation;
import com.sun.identity.saml2.assertion.SubjectConfirmationData;
import com.sun.identity.saml2.common.AccountUtils;
import com.sun.identity.saml2.common.NameIDInfo;
import com.sun.identity.saml2.common.NewBoolean;
import com.sun.identity.saml2.common.SAML2Constants;
import com.sun.identity.saml2.common.SAML2Exception;
import com.sun.identity.saml2.common.SAML2Repository;
import com.sun.identity.saml2.common.SAML2Utils;
import com.sun.identity.saml2.ecp.ECPFactory;
import com.sun.identity.saml2.ecp.ECPResponse;
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.plugins.IDPAccountMapper;
import com.sun.identity.saml2.plugins.IDPAttributeMapper;
import com.sun.identity.saml2.plugins.IDPAuthnContextMapper;
import com.sun.identity.saml2.plugins.IDPECPSessionMapper;
import com.sun.identity.saml2.protocol.Artifact;
import com.sun.identity.saml2.protocol.AuthnRequest;
import com.sun.identity.saml2.protocol.NameIDPolicy;
import com.sun.identity.saml2.protocol.ProtocolFactory;
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.DateUtils;
import com.sun.identity.shared.encode.URLEncDec;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.xml.soap.SOAPMessage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.regex.Pattern;

public class IDPSSOUtil {
   public static final String NULL = "null";
   public static final SAML2MetaManager META_MANAGER = createMetaManager();
   private static final IDPSessionListener SESSION_LISTENER = new IDPSessionListener();
   private static SessionProvider sessionProvider;
   private static final Pattern CONTROL_CHARS_PATTERN = Pattern.compile("\\p{C}");

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

   public static void doSSOFederate(
      HttpServletRequest request,
      HttpServletResponse response,
      AuthnRequest authnReq,
      String spEntityID,
      String idpMetaAlias,
      String nameIDFormat,
      String relayState
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.doSSOFederate: ";
      Object session = null;

      try {
         session = sessionProvider.getSession(request);
      } catch (SessionException se) {
         if (SAML2Utils.DEBUG.warningEnabled()) {
            SAML2Utils.DEBUG.warning(classMethod + "No session yet.");
         }
      }

      String authnRequestStr = null;
      if (authnReq != null) {
         authnRequestStr = authnReq.toXMLString();
      }

      String[] logdata = new String[]{spEntityID, idpMetaAlias, authnRequestStr};
      LogUtil.access(Level.INFO, "RECEIVED_AUTHN_REQUEST", logdata, session);
      String idpEntityID = null;
      String realm = null;

      try {
         if (META_MANAGER == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
         }

         idpEntityID = META_MANAGER.getEntityByMetaAlias(idpMetaAlias);
         if (idpEntityID == null || idpEntityID.trim().isEmpty()) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get IDP Entity ID from meta.");
            String[] data = new String[]{idpEntityID};
            LogUtil.error(Level.INFO, "INVALID_IDP", data, session);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }

         realm = SAML2MetaUtils.getRealmByMetaAlias(idpMetaAlias);
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get IDP Entity ID from meta.");
         String[] data = new String[]{idpMetaAlias};
         LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, session);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      if (authnReq == null) {
         Issuer issuer = AssertionFactory.getInstance().createIssuer();
         issuer.setValue(spEntityID);
         if (!SAML2Utils.isSourceSiteValid(issuer, realm, idpEntityID)) {
            if (SAML2Utils.DEBUG.warningEnabled()) {
               SAML2Utils.DEBUG.warning(classMethod + "The remote provider is not valid.");
            }

            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidReceiver"));
         }
      }

      if (authnReq == null && session == null) {
         try {
            redirectAuthentication(response, null);
         } catch (IOException ioe) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to redirect to authentication.", ioe);
            SAMLUtils.sendError(response, "UnableToRedirectToAuth", SAML2Utils.BUNDLE.getString("UnableToRedirectToAuth"));
         }
      } else {
         sendResponseToACS(request, response, session, authnReq, spEntityID, idpEntityID, idpMetaAlias, realm, nameIDFormat, relayState, null);
      }
   }

   public static void sendResponseToACS(
      HttpServletRequest request,
      HttpServletResponse response,
      Object session,
      AuthnRequest authnReq,
      String spEntityID,
      String idpEntityID,
      String idpMetaAlias,
      String realm,
      String nameIDFormat,
      String relayState,
      AuthnContext matchingAuthnContext
   ) throws SAML2Exception {
      StringBuffer returnedBinding = new StringBuffer();
      String acsURL = getACSurl(spEntityID, idpEntityID, realm, authnReq, request, returnedBinding);
      String acsBinding = returnedBinding.toString();
      if (acsURL == null || acsURL.trim().isEmpty()) {
         SAML2Utils.DEBUG.error("IDPSSOUtil.sendResponseToACS: no ACS URL found.");
         String[] data = new String[]{idpMetaAlias};
         LogUtil.error(Level.INFO, "NO_ACS_URL", data, session);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("UnableTofindACSURL"));
      }

      if (acsBinding != null && !acsBinding.trim().isEmpty()) {
         String affiliationID = request.getParameter("affiliationID");
         Response res = getResponse(session, authnReq, spEntityID, idpEntityID, idpMetaAlias, realm, nameIDFormat, acsURL, affiliationID, matchingAuthnContext);
         if (res == null) {
            SAML2Utils.DEBUG.error("IDPSSOUtil.sendResponseToACS: response is null");
            String errorMsg = SAML2Utils.BUNDLE.getString("UnableToCreateAssertion");
            if (authnReq == null) {
               throw new SAML2Exception(errorMsg);
            }

            res = SAML2Utils.getErrorResponse(authnReq, "urn:oasis:names:tc:SAML:2.0:status:Responder", null, errorMsg, idpEntityID);
         } else {
            try {
               String[] values = new String[]{idpMetaAlias};
               sessionProvider.setProperty(session, "IDPMetaAlias", values);
            } catch (SessionException e) {
               SAML2Utils.DEBUG.error("IDPSSOUtil.sendResponseToACS: error setting idpMetaAlias into the session: ", e);
            }
         }

         if (res != null) {
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("IDPSSOUtil.sendResponseToACS: Response is:  " + res.toXMLString());
            }

            sendResponse(request, response, acsBinding, spEntityID, idpEntityID, idpMetaAlias, realm, relayState, acsURL, res, session);
         } else {
            SAML2Utils.DEBUG.error("IDPSSOUtil.sendResponseToACS: error response is null");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("UnableToCreateErrorResponse"));
         }
      } else {
         SAML2Utils.DEBUG.error("IDPSSOUtil.sendResponseToACS: no return binding found.");
         String[] data = new String[]{idpMetaAlias};
         LogUtil.error(Level.INFO, "NO_RETURN_BINDING", data, session);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("UnableTofindBinding"));
      }
   }

   public static void sendResponse(
      HttpServletRequest request,
      HttpServletResponse response,
      String acsBinding,
      String spEntityID,
      String idpEntityID,
      String idpMetaAlias,
      String realm,
      String relayState,
      String acsURL,
      Response res,
      Object session
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.sendResponse: ";
      String nameIDString = SAML2Utils.getNameIDStringFromResponse(res);
      Map<String, String> props = new HashMap<>();
      props.put("NameID", nameIDString);
      if (acsBinding.equals("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")) {
         boolean signAssertion = true;
         boolean signResponse = SAML2Utils.wantPOSTResponseSigned(realm, spEntityID, "SPRole");
         if (signResponse) {
            signAssertion = false;
         }

         signAndEncryptResponseComponents(realm, spEntityID, idpEntityID, res, signAssertion);
         if (signResponse) {
            signResponse(realm, idpEntityID, res);
         }

         String resMsg = res.toXMLString(true, true);
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message(classMethod + "SAML Response content :\n" + resMsg);
         }

         String encodedResMsg = SAML2Utils.encodeForPOST(resMsg);
         String[] logdata1 = new String[]{spEntityID, idpMetaAlias};
         LogUtil.access(Level.INFO, "POST_RESPONSE", logdata1, session, props);

         try {
            SAML2Utils.postToTarget(request, response, "SAMLResponse", encodedResMsg, "RelayState", relayState, acsURL);
         } catch (Exception e) {
            SAML2Utils.DEBUG.error(classMethod + "postToTarget failed.", e);
            String[] data = new String[]{acsURL};
            LogUtil.error(Level.INFO, "POST_TO_TARGET_FAILED", data, session, props);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("postToTargetFailed"));
         }
      } else if (acsBinding.equals("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact")) {
         sendResponseArtifact(request, response, idpEntityID, spEntityID, realm, acsURL, relayState, res, session, props);
      } else {
         if (!acsBinding.equals("urn:oasis:names:tc:SAML:2.0:bindings:PAOS")) {
            SAML2Utils.DEBUG.error(classMethod + "unsupported return binding.");
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("UnSupportedReturnBinding"));
         }

         signAndEncryptResponseComponents(realm, spEntityID, idpEntityID, res, true);
         sendResponseECP(response, idpEntityID, realm, acsURL, res);
      }
   }

   public static Response getResponse(
      Object session,
      AuthnRequest authnReq,
      String recipientEntityID,
      String idpEntityID,
      String idpMetaAlias,
      String realm,
      String nameIDFormat,
      String acsURL,
      String affiliationID,
      AuthnContext matchingAuthnContext
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getResponse: ";
      Response res = ProtocolFactory.getInstance().createResponse();
      Status status = ProtocolFactory.getInstance().createStatus();
      if (status == null) {
         return null;
      }

      StatusCode statusCode = ProtocolFactory.getInstance().createStatusCode();
      if (statusCode == null) {
         return null;
      }

      List<Assertion> assertionList = new ArrayList<>();
      Assertion assertion = getAssertion(
         session, authnReq, recipientEntityID, idpEntityID, idpMetaAlias, realm, nameIDFormat, acsURL, affiliationID, matchingAuthnContext
      );
      if (assertion == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get Assertion.");
         return null;
      }

      assertionList.add(assertion);
      res.setAssertion(assertionList);
      statusCode.setValue("urn:oasis:names:tc:SAML:2.0:status:Success");
      status.setStatusCode(statusCode);
      res.setStatus(status);
      if (authnReq != null) {
         res.setInResponseTo(authnReq.getID());
      }

      res.setVersion("2.0");
      res.setIssueInstant(new Date());
      res.setID(SAML2Utils.generateID());
      Issuer issuer = AssertionFactory.getInstance().createIssuer();
      issuer.setValue(idpEntityID);
      res.setIssuer(issuer);
      res.setDestination(acsURL);
      return res;
   }

   private static Assertion getAssertion(
      Object session,
      AuthnRequest authnReq,
      String recipientEntityID,
      String idpEntityID,
      String idpMetaAlias,
      String realm,
      String nameIDFormat,
      String acsURL,
      String affiliationID,
      AuthnContext matchingAuthnContext
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getAssertion: ";
      Assertion assertion = AssertionFactory.getInstance().createAssertion();
      String assertionID = SAML2Utils.generateID();
      assertion.setID(assertionID);
      assertion.setVersion("2.0");
      assertion.setIssueInstant(new Date());
      Issuer issuer = AssertionFactory.getInstance().createIssuer();
      issuer.setValue(idpEntityID);
      assertion.setIssuer(issuer);
      List<AuthnStatement> statementList = new ArrayList<>();
      NewBoolean isNewSessionIndex = new NewBoolean();
      AuthnStatement authnStatement = getAuthnStatement(session, isNewSessionIndex, idpEntityID, realm, matchingAuthnContext);
      if (authnStatement == null) {
         return null;
      }

      String sessionIndex = authnStatement.getSessionIndex();
      IDPSession idpSession = null;
      if (isNewSessionIndex.getValue()) {
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG
               .message(
                  classMethod + "This is a new IDP session with sessionIndex=" + sessionIndex + ", and sessionID=" + sessionProvider.getSessionID(session)
               );
         }

         idpSession = new IDPSession(session);
         idpSession.setMetaAlias(idpMetaAlias);
         IDPCache.idpSessionsByIndices().put(sessionIndex, idpSession);
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message(classMethod + "a new IDP session has been saved in cache, with sessionIndex=" + sessionIndex);
         }

         try {
            sessionProvider.addListener(session, SESSION_LISTENER);
         } catch (SessionException e) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to add session listener.");
         }
      } else {
         idpSession = IDPCache.idpSessionsByIndices().get(sessionIndex);
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG
               .message(
                  classMethod
                     + "This is an existing IDP session with sessionIndex="
                     + sessionIndex
                     + ", and sessionID="
                     + sessionProvider.getSessionID(idpSession.getSession())
               );
         }
      }

      statementList.add(authnStatement);
      AttributeStatement attrStatement = getAttributeStatement(session, idpEntityID, recipientEntityID, realm);
      if (attrStatement != null) {
         List<AttributeStatement> attrStatementList = new ArrayList<>();
         attrStatementList.add(attrStatement);
         assertion.setAttributeStatements(attrStatementList);
      }

      int effectiveTime = getEffectiveTime(realm, idpEntityID);
      int notBeforeSkewTime = getNotBeforeSkewTime(realm, idpEntityID);
      NewBoolean isNewFederation = new NewBoolean();
      Subject subject = getSubject(
         session, authnReq, acsURL, nameIDFormat, isNewFederation, realm, idpEntityID, recipientEntityID, effectiveTime, affiliationID
      );
      String spEntityID = null;
      if (authnReq != null) {
         spEntityID = authnReq.getIssuer().getValue();
      } else {
         spEntityID = recipientEntityID;
      }

      NameIDandSPpair pair = new NameIDandSPpair(subject.getNameID(), spEntityID);
      synchronized (Saml2DataMapProvider.DataMapId.IDP_IDP_SESSIONS_BY_INDICES) {
         List<NameIDandSPpair> list = idpSession.getNameIDandSPpairs();
         if (isNewFederation.getValue()) {
            list.add(pair);
            IDPCache.idpSessionsByIndices().put(sessionIndex, idpSession);
         } else {
            String id = authnReq != null ? authnReq.getIssuer().getValue() : spEntityID;
            NameIDandSPpair p = null;

            for (NameIDandSPpair nameIDandSPpair : list) {
               p = nameIDandSPpair;
               if (p.getSPEntityID().equals(id)) {
                  break;
               }

               p = null;
            }

            if (p == null) {
               list.add(pair);
               IDPCache.idpSessionsByIndices().put(sessionIndex, idpSession);
            }
         }
      }

      assertion.setAuthnStatements(statementList);
      assertion.setSubject(subject);
      Conditions conditions = getConditions(recipientEntityID, notBeforeSkewTime, effectiveTime);
      assertion.setConditions(conditions);

      try {
         long sessionExpireTime = System.currentTimeMillis() / 1000L + sessionProvider.getTimeLeft(session);
         if (SAML2Utils.isSAML2FailOverEnabled()) {
            SAML2Repository.getInstance().save(sessionIndex, new IDPSessionCopy(IDPCache.idpSessionsByIndices().get(sessionIndex)), sessionExpireTime, null);
         }

         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message("SAVE IDPSession!");
         }
      } catch (SAML2Exception e) {
         SAML2Utils.DEBUG.error(classMethod + "DB error!");
      } catch (SessionException se) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get left-time from the session.", se);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidSSOToken"));
      }

      return assertion;
   }

   private static AuthnStatement getAuthnStatement(
      Object session, NewBoolean isNewSessionIndex, String idpEntityID, String realm, AuthnContext matchingAuthnContext
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getAuthnStatement: ";
      AuthnStatement authnStatement = AssertionFactory.getInstance().createAuthnStatement();
      Date authInstant = null;

      try {
         String[] values = sessionProvider.getProperty(session, "authInstant");
         if (values != null && values.length != 0 && values[0] != null && !values[0].isEmpty()) {
            authInstant = DateUtils.stringToDate(values[0]);
         }
      } catch (Exception e) {
         SAML2Utils.DEBUG.error(classMethod + "exception retrieving info from the session: ", e);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorGettingAuthnStatement"));
      }

      if (authInstant == null) {
         authInstant = new Date();
      }

      authnStatement.setAuthnInstant(authInstant);
      AuthnContext authnContext = matchingAuthnContext;
      if (authnContext == null) {
         String authLevel = null;

         try {
            String[] values = sessionProvider.getProperty(session, "AuthLevel");
            if (values != null && values.length != 0 && values[0] != null && !values[0].isEmpty()) {
               authLevel = values[0];
            }
         } catch (Exception e) {
            SAML2Utils.DEBUG.error(classMethod + "exception retrieving auth level info from the session: ", e);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorGettingAuthnStatement"));
         }

         IDPAuthnContextMapper idpAuthnContextMapper = getIDPAuthnContextMapper(realm, idpEntityID);
         authnContext = idpAuthnContextMapper.getAuthnContextFromAuthLevel(authLevel, realm, idpEntityID);
      }

      authnStatement.setAuthnContext(authnContext);
      String sessionIndex = getSessionIndex(session);
      if (sessionIndex == null) {
         sessionIndex = SAML2Utils.generateIDWithServerID();

         try {
            String[] values = new String[]{sessionIndex};
            sessionProvider.setProperty(session, "SAML2IDPSessionIndex", values);
         } catch (SessionException e) {
            SAML2Utils.DEBUG.error(classMethod + "error setting session index into the session: ", e);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorGettingAuthnStatement"));
         }

         isNewSessionIndex.setValue(true);
      } else {
         isNewSessionIndex.setValue(false);
      }

      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message(classMethod + "SessionIndex (in AuthnStatement) =" + sessionIndex);
      }

      if (sessionIndex != null) {
         Set<AuthnContext> authContextSet = IDPCache.authnContextCache().get(sessionIndex);
         if (authContextSet == null || authContextSet.isEmpty()) {
            authContextSet = new HashSet<>();
         }

         authContextSet.add(authnContext);
         IDPCache.authnContextCache().put(sessionIndex, authContextSet);
         authnStatement.setSessionIndex(sessionIndex);
      }

      return authnStatement;
   }

   private static AttributeStatement getAttributeStatement(Object session, String idpEntityID, String recipientEntityID, String realm) throws SAML2Exception {
      IDPAttributeMapper idpAttrMapper = getIDPAttributeMapper(realm, idpEntityID);
      List<Attribute> attributes = idpAttrMapper.getAttributes(session, idpEntityID, recipientEntityID, realm);
      if (attributes != null && !attributes.isEmpty()) {
         AttributeStatement attrStatement = AssertionFactory.getInstance().createAttributeStatement();
         attrStatement.setAttribute(attributes);
         return attrStatement;
      } else {
         return null;
      }
   }

   private static IDPAttributeMapper getIDPAttributeMapper(String realm, String idpEntityID) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getIDPAttributeMapper: ";
      String idpAttributeMapperName = null;
      IDPAttributeMapper idpAttributeMapper = null;

      try {
         idpAttributeMapperName = getAttributeValueFromIDPSSOConfig(realm, idpEntityID, "idpAttributeMapper");
         if (idpAttributeMapperName == null) {
            idpAttributeMapperName = SAML2Constants.DEFAULT_IDP_ATTRIBUTE_MAPPER_CLASS;
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message(classMethod + "use " + SAML2Constants.DEFAULT_IDP_ATTRIBUTE_MAPPER_CLASS);
            }
         }

         idpAttributeMapper = IDPCache.idpAttributeMapperCache.get(idpAttributeMapperName);
         if (idpAttributeMapper == null) {
            idpAttributeMapper = (IDPAttributeMapper)Class.forName(idpAttributeMapperName).getDeclaredConstructor().newInstance();
            IDPCache.idpAttributeMapperCache.put(idpAttributeMapperName, idpAttributeMapper);
         } else if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message(classMethod + "got the IDPAttributeMapper from cache");
         }

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

   static IDPAuthnContextMapper getIDPAuthnContextMapper(String realm, String idpEntityID) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getIDPAuthnContextMapper: ";
      String idpAuthnContextMapperName = null;
      IDPAuthnContextMapper idpAuthnContextMapper = null;

      try {
         idpAuthnContextMapperName = getAttributeValueFromIDPSSOConfig(realm, idpEntityID, "idpAuthncontextMapper");
         if (idpAuthnContextMapperName == null) {
            idpAuthnContextMapperName = SAML2Constants.DEFAULT_IDP_AUTHNCONTEXT_MAPPER_CLASS;
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message(classMethod + "use " + SAML2Constants.DEFAULT_IDP_AUTHNCONTEXT_MAPPER_CLASS);
            }
         }

         idpAuthnContextMapper = IDPCache.idpAuthnContextMapperCache.get(idpAuthnContextMapperName);
         if (idpAuthnContextMapper == null) {
            idpAuthnContextMapper = (IDPAuthnContextMapper)Class.forName(idpAuthnContextMapperName).getDeclaredConstructor().newInstance();
            IDPCache.idpAuthnContextMapperCache.put(idpAuthnContextMapperName, idpAuthnContextMapper);
         } else if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message(classMethod + "got the IDPAuthnContextMapper from cache");
         }

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

   static IDPECPSessionMapper getIDPECPSessionMapper(String realm, String idpEntityID) throws SAML2Exception {
      String idpECPSessionMapperName = null;
      IDPECPSessionMapper idpECPSessionMapper = null;

      try {
         idpECPSessionMapperName = getAttributeValueFromIDPSSOConfig(realm, idpEntityID, "idpECPSessionMapper");
         if (idpECPSessionMapperName == null) {
            idpECPSessionMapperName = SAML2Constants.DEFAULT_IDP_ECP_SESSION_MAPPER_CLASS;
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("IDPSSOUtil.getIDPECPSessionMapper: use " + SAML2Constants.DEFAULT_IDP_ECP_SESSION_MAPPER_CLASS);
            }
         }

         idpECPSessionMapper = IDPCache.idpECPSessionMapperCache.get(idpECPSessionMapperName);
         if (idpECPSessionMapper == null) {
            idpECPSessionMapper = (IDPECPSessionMapper)Class.forName(idpECPSessionMapperName).getDeclaredConstructor().newInstance();
            IDPCache.idpECPSessionMapperCache.put(idpECPSessionMapperName, idpECPSessionMapper);
         } else if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message("IDPSSOUtil.getIDPECPSessionMapper: got the IDPECPSessionMapper from cache");
         }

         return idpECPSessionMapper;
      } catch (Exception ex) {
         SAML2Utils.DEBUG.error("IDPSSOUtil.getIDPECPSessionMapper: Unable to get IDPECPSessionMapper.", ex);
         throw new SAML2Exception(ex);
      }
   }

   private static Subject getSubject(
      Object session,
      AuthnRequest authnReq,
      String acsURL,
      String nameIDFormat,
      NewBoolean isNewFederation,
      String realm,
      String idpEntityID,
      String recipientEntityID,
      int effectiveTime,
      String affiliationID
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getSubject: ";
      Subject subject = AssertionFactory.getInstance().createSubject();

      try {
         sessionProvider.getPrincipalName(session);
      } catch (SessionException se) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get principal name from the session.", se);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidSSOToken"));
      }

      String spNameQualifier = null;
      String remoteEntityID;
      if (authnReq != null) {
         remoteEntityID = authnReq.getIssuer().getValue();
         NameIDPolicy nameIDPolicy = authnReq.getNameIDPolicy();
         if (nameIDPolicy != null) {
            spNameQualifier = nameIDPolicy.getSPNameQualifier();
            if (spNameQualifier != null) {
               AffiliationDescriptorType affiDesc = META_MANAGER.getAffiliationDescriptor(realm, spNameQualifier);
               if (affiDesc != null) {
                  if (!affiDesc.getAffiliateMember().contains(remoteEntityID)) {
                     throw new SAML2Exception(SAML2Utils.BUNDLE.getString("spNotAffiliationMember"));
                  }

                  remoteEntityID = spNameQualifier;
               }
            }
         }
      } else if (affiliationID != null) {
         AffiliationDescriptorType affiDesc = META_MANAGER.getAffiliationDescriptor(realm, affiliationID);
         if (affiDesc == null) {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("affiliationNotFound"));
         }

         if (!affiDesc.getAffiliateMember().contains(recipientEntityID)) {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("spNotAffiliationMember"));
         }

         remoteEntityID = affiliationID;
      } else {
         remoteEntityID = recipientEntityID;
      }

      SPSSODescriptorType spsso = META_MANAGER.getSPSSODescriptor(realm, recipientEntityID);
      if (spsso == null) {
         String[] data = new String[]{recipientEntityID};
         LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      IDPSSODescriptorType idpsso = META_MANAGER.getIDPSSODescriptor(realm, idpEntityID);
      if (idpsso == null) {
         String[] data = new String[]{idpEntityID};
         LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      nameIDFormat = SAML2Utils.verifyNameIDFormat(nameIDFormat, spsso, idpsso);
      NameIDInfo nameIDInfo = null;
      NameID nameID = null;
      boolean isTransient = nameIDFormat.equals("urn:oasis:names:tc:SAML:2.0:nameid-format:transient");
      if (!isTransient) {
         String userID = null;

         try {
            userID = sessionProvider.getPrincipalName(session);
         } catch (SessionException se) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get principal name from the session.", se);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidSSOToken"));
         }

         nameIDInfo = AccountUtils.getAccountFederation(userID, idpEntityID, remoteEntityID);
         if (nameIDInfo != null) {
            nameID = nameIDInfo.getNameID();
            if (nameIDFormat.equals(nameID.getFormat())) {
               isNewFederation.setValue(false);
            } else {
               nameID = null;
            }
         }
      }

      if (nameID == null) {
         IDPAccountMapper idpAccountMapper = SAML2Utils.getIDPAccountMapper(realm, idpEntityID);
         nameID = idpAccountMapper.getNameID(session, idpEntityID, remoteEntityID, realm, nameIDFormat);
         if (nameIDFormat.equals("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent")) {
            isNewFederation.setValue(false);
         } else {
            isNewFederation.setValue(!isTransient);
         }
      }

      subject.setNameID(nameID);
      String inResponseTo = null;
      if (authnReq != null) {
         inResponseTo = authnReq.getID();
      }

      SubjectConfirmation sc = getSubjectConfirmation(inResponseTo, acsURL, effectiveTime);
      if (sc == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get subject confirmation");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("noSubjectConfirmation"));
      } else {
         List<SubjectConfirmation> list = new ArrayList<>();
         list.add(sc);
         subject.setSubjectConfirmation(list);
         return subject;
      }
   }

   private static SubjectConfirmation getSubjectConfirmation(String inResponseTo, String acsURL, int effectiveTime) throws SAML2Exception {
      SubjectConfirmation sc = AssertionFactory.getInstance().createSubjectConfirmation();
      sc.setMethod("urn:oasis:names:tc:SAML:2.0:cm:bearer");
      SubjectConfirmationData scd = AssertionFactory.getInstance().createSubjectConfirmationData();
      scd.setRecipient(acsURL);
      if (inResponseTo != null) {
         scd.setInResponseTo(inResponseTo);
      }

      Date date = new Date();
      date.setTime(date.getTime() + effectiveTime * 1000L);
      scd.setNotOnOrAfter(date);
      sc.setSubjectConfirmationData(scd);
      return sc;
   }

   protected static Conditions getConditions(String audienceEntityID, int notBeforeSkewTime, int effectiveTime) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getConditions: ";
      Conditions conditions = AssertionFactory.getInstance().createConditions();
      Date date = new Date();
      date.setTime(date.getTime() - notBeforeSkewTime * 1000L);
      conditions.setNotBefore(date);
      date = new Date();
      date.setTime(date.getTime() + effectiveTime * 1000L);
      conditions.setNotOnOrAfter(date);
      List<AudienceRestriction> list = new ArrayList<>();
      AudienceRestriction ar = getAudienceRestriction(audienceEntityID);
      if (ar == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get Audience Restriction");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("noAudienceRestriction"));
      } else {
         list.add(ar);
         conditions.setAudienceRestrictions(list);
         return conditions;
      }
   }

   private static AudienceRestriction getAudienceRestriction(String audienceEntityID) throws SAML2Exception {
      AudienceRestriction ar = AssertionFactory.getInstance().createAudienceRestriction();
      if (audienceEntityID != null) {
         List<String> list = new ArrayList<>();
         list.add(audienceEntityID);
         ar.setAudience(list);
      }

      return ar;
   }

   public static String getACSurl(String spEntityID, String idpEntityID, String realm, AuthnRequest authnReq, HttpServletRequest request, StringBuffer rBinding) throws SAML2Exception {
      String providedAcsUrl = null;
      String providedAcsBinding;
      if (authnReq != null) {
         providedAcsUrl = authnReq.getAssertionConsumerServiceURL();
         providedAcsBinding = authnReq.getProtocolBinding();
      } else {
         providedAcsBinding = request.getParameter("binding");
      }

      if (providedAcsBinding != null && !providedAcsBinding.trim().isEmpty() && !providedAcsBinding.startsWith("urn:oasis:names:tc:SAML:2.0:bindings:")) {
         providedAcsBinding = "urn:oasis:names:tc:SAML:2.0:bindings:" + providedAcsBinding;
      }

      StringBuffer returnedBinding = new StringBuffer();
      String validatedAcsUrl;
      String acsBinding;
      if (providedAcsUrl != null && !providedAcsUrl.isEmpty()) {
         validatedAcsUrl = validateAcsUrl(providedAcsUrl, spEntityID, idpEntityID, realm);
         if (providedAcsBinding != null && !providedAcsBinding.trim().isEmpty()) {
            acsBinding = providedAcsBinding;
         } else {
            acsBinding = getBindingForAcsUrl(spEntityID, realm, validatedAcsUrl);
         }
      } else {
         if (providedAcsBinding != null && !providedAcsBinding.trim().isEmpty()) {
            validatedAcsUrl = getACSurlFromMetaByBinding(spEntityID, realm, providedAcsBinding, returnedBinding);
         } else {
            int acsIndex = 0;
            if (authnReq != null) {
               Integer acsIndexInteger = authnReq.getAssertionConsumerServiceIndex();
               if (acsIndexInteger == null) {
                  validatedAcsUrl = getDefaultACSurl(spEntityID, realm, returnedBinding);
               } else {
                  acsIndex = acsIndexInteger;
                  if (acsIndex < 0 || acsIndex > 65535) {
                     acsIndex = 0;
                  }

                  validatedAcsUrl = getACSurlFromMetaByIndex(spEntityID, realm, acsIndex, returnedBinding);
               }
            } else {
               validatedAcsUrl = getDefaultACSurl(spEntityID, realm, returnedBinding);
            }
         }

         acsBinding = returnedBinding.toString();
      }

      rBinding.append(acsBinding);
      return validatedAcsUrl;
   }

   private static String validateAcsUrl(String url, String spEntityID, String idpEntityID, String realm) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.assertValidAcsUrl: ";
      SPSSODescriptorType spSSODescriptorElement = null;
      if (META_MANAGER == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
      }

      try {
         spSSODescriptorElement = META_MANAGER.getSPSSODescriptor(realm, spEntityID);
         if (spSSODescriptorElement == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
         String[] data = new String[]{spEntityID};
         LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      for (IndexedEndpointType assertionConsumerServiceElement : spSSODescriptorElement.getAssertionConsumerService()) {
         IndexedEndpointType acs = assertionConsumerServiceElement;
         if (acs.getLocation().equals(url)) {
            return url;
         }
      }

      IDPSSODescriptorType idpSSODescriptor;
      try {
         idpSSODescriptor = META_MANAGER.getIDPSSODescriptor(realm, idpEntityID);
      } catch (SAML2MetaException sme) {
         throw new SAML2Exception(String.format("Error finding metadata for idp entity id '%s': %s", idpEntityID, sme.getMessage()));
      }

      String message = String.format(
         "The provided ACS URL in the AuthnRequest '%s' matches no configured ACS URL of the SP with entity id '%s'",
         logSafeString(url),
         logSafeString(spEntityID)
      );
      if (!XjcHelper.mapBooleanWrapperToPrimitiveWithFalseAsDefault(idpSSODescriptor.isWantAuthnRequestsSigned())
         && !Boolean.getBoolean("iam.saml2.lenientAcsValidation")) {
         throw new SAML2Exception(message);
      }

      LogUtil.error(Level.WARNING, message, null);
      return url;
   }

   private static String logSafeString(String str) {
      return CONTROL_CHARS_PATTERN.matcher(str).replaceAll("?");
   }

   private static String getDefaultACSurl(String spEntityID, String realm, StringBuffer returnedBinding) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getDefaultACSurl: ";
      SPSSODescriptorType spSSODescriptorElement = null;
      if (META_MANAGER == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
      }

      try {
         spSSODescriptorElement = META_MANAGER.getSPSSODescriptor(realm, spEntityID);
         if (spSSODescriptorElement == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
         String[] data = new String[]{spEntityID};
         LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      List<IndexedEndpointType> acsList = spSSODescriptorElement.getAssertionConsumerService();
      IndexedEndpointType acs = null;
      String acsURL = null;
      String binding = null;
      String firstAcsURL = null;
      String firstBinding = null;

      for (int i = 0; i < acsList.size(); i++) {
         acs = acsList.get(i);
         if (XjcHelper.mapBooleanWrapperToPrimitiveWithFalseAsDefault(acs.isIsDefault())) {
            acsURL = acs.getLocation();
            binding = acs.getBinding();
         }

         if (i == 0) {
            firstAcsURL = acs.getLocation();
            firstBinding = acs.getBinding();
         }
      }

      if (acsURL == null) {
         acsURL = firstAcsURL;
         binding = firstBinding;
      }

      if (binding != null) {
         returnedBinding.append(binding);
      }

      return acsURL;
   }

   private static String getBindingForAcsUrl(String spEntityID, String realm, String acsURL) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getBindingForAcsUrl: ";
      SPSSODescriptorType spSSODescriptorElement = null;
      if (META_MANAGER == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
      }

      try {
         spSSODescriptorElement = META_MANAGER.getSPSSODescriptor(realm, spEntityID);
         if (spSSODescriptorElement == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
         String[] data = new String[]{spEntityID};
         LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      List<IndexedEndpointType> acsList = spSSODescriptorElement.getAssertionConsumerService();
      IndexedEndpointType acs = null;

      for (IndexedEndpointType assertionConsumerServiceElement : acsList) {
         acs = assertionConsumerServiceElement;
         String location = acs.getLocation();
         if (location != null && location.equals(acsURL)) {
            return acs.getBinding();
         }
      }

      return null;
   }

   private static String getACSurlFromMetaByBinding(String spEntityID, String realm, String desiredBinding, StringBuffer returnedBinding) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getACSurlFromMetaByBinding: ";
      SPSSODescriptorType spSSODescriptorElement = null;
      if (META_MANAGER == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
      }

      try {
         spSSODescriptorElement = META_MANAGER.getSPSSODescriptor(realm, spEntityID);
         if (spSSODescriptorElement == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
         String[] data = new String[]{spEntityID};
         LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      List<IndexedEndpointType> acsList = spSSODescriptorElement.getAssertionConsumerService();
      String acsURL = null;
      String binding = null;
      String defaultAcsURL = null;
      String defaultBinding = null;
      String firstAcsURL = null;
      String firstBinding = null;
      IndexedEndpointType acs = null;

      for (int i = 0; i < acsList.size(); i++) {
         acs = acsList.get(i);
         binding = acs.getBinding();
         if (binding.equals(desiredBinding)) {
            acsURL = acs.getLocation();
            break;
         }

         if (XjcHelper.mapBooleanWrapperToPrimitiveWithFalseAsDefault(acs.isIsDefault())) {
            defaultAcsURL = acs.getLocation();
            defaultBinding = acs.getBinding();
         }

         if (i == 0) {
            firstAcsURL = acs.getLocation();
            firstBinding = acs.getBinding();
         }
      }

      if (acsURL != null && !acsURL.isEmpty()) {
         returnedBinding.append(binding);
      } else {
         acsURL = defaultAcsURL;
         if (acsURL != null && !acsURL.isEmpty()) {
            returnedBinding.append(defaultBinding);
         } else {
            acsURL = firstAcsURL;
            if (acsURL == null || acsURL.isEmpty()) {
               SAML2Utils.DEBUG.error(classMethod + "Unable to get valid Assertion Consumer Service URL");
               return null;
            }

            returnedBinding.append(firstBinding);
         }
      }

      return acsURL;
   }

   private static String getACSurlFromMetaByIndex(String spEntityID, String realm, int acsIndex, StringBuffer returnedBinding) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.getACSurlFromMetaByIndex: ";
      SPSSODescriptorType spSSODescriptorElement = null;
      if (META_MANAGER == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
      }

      try {
         spSSODescriptorElement = META_MANAGER.getSPSSODescriptor(realm, spEntityID);
         if (spSSODescriptorElement == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
         String[] data = new String[]{spEntityID};
         LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      List<IndexedEndpointType> acsList = spSSODescriptorElement.getAssertionConsumerService();
      String acsURL = null;
      String binding = null;
      String defaultAcsURL = null;
      String defaultBinding = null;
      String firstAcsURL = null;
      String firstBinding = null;
      IndexedEndpointType acs = null;

      for (int i = 0; i < acsList.size(); i++) {
         acs = acsList.get(i);
         int index = acs.getIndex();
         binding = acs.getBinding();
         if (index == acsIndex) {
            acsURL = acs.getLocation();
            binding = acs.getBinding();
            break;
         }

         if (XjcHelper.mapBooleanWrapperToPrimitiveWithFalseAsDefault(acs.isIsDefault())) {
            defaultAcsURL = acs.getLocation();
            defaultBinding = acs.getBinding();
         }

         if (i == 0) {
            firstAcsURL = acs.getLocation();
            firstBinding = acs.getBinding();
         }
      }

      if (acsURL != null && !acsURL.isEmpty()) {
         returnedBinding.append(binding);
      } else {
         acsURL = defaultAcsURL;
         if (acsURL != null && !acsURL.isEmpty()) {
            returnedBinding.append(defaultBinding);
         } else {
            acsURL = firstAcsURL;
            if (acsURL == null || acsURL.isEmpty()) {
               SAML2Utils.DEBUG.error(classMethod + "Unable to get valid Assertion Consumer Service URL");
               return null;
            }

            returnedBinding.append(firstBinding);
         }
      }

      return acsURL;
   }

   private static void sendResponseArtifact(
      HttpServletRequest request,
      HttpServletResponse response,
      String idpEntityID,
      String spEntityID,
      String realm,
      String acsURL,
      String relayState,
      Response res,
      Object session,
      Map<String, String> props
   ) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.sendResponseArtifact: ";
      IDPSSODescriptorType idpSSODescriptorElement = null;

      try {
         idpSSODescriptorElement = META_MANAGER.getIDPSSODescriptor(realm, idpEntityID);
         if (idpSSODescriptorElement == null) {
            SAML2Utils.DEBUG.error(classMethod + "Unable to get IDP SSO Descriptor from meta.");
            String[] data = new String[]{idpEntityID};
            LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, session, props);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }
      } catch (SAML2MetaException sme) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get IDP SSO Descriptor from meta.");
         String[] data = new String[]{idpEntityID};
         LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, session, props);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      IndexedEndpointType ars = idpSSODescriptorElement.getArtifactResolutionService().getFirst();
      if (ars == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get ArtifactResolutionServiceElement from meta.");
         String[] data = new String[]{idpEntityID};
         LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, session, props);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }

      Artifact art = null;

      try {
         art = ProtocolFactory.getInstance()
            .createArtifact(null, ars.getIndex(), SAML2Utils.generateSourceID(idpEntityID), SAML2Utils.generateMessageHandleWithServerID());
      } catch (SAML2Exception se) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to create artifact: ", se);
         String[] data = new String[]{idpEntityID};
         LogUtil.error(Level.INFO, "CANNOT_CREATE_ARTIFACT", data, session, props);
         SAMLUtils.sendError(response, "errorCreateArtifact", SAML2Utils.BUNDLE.getString("errorCreateArtifact"));
         return;
      }

      String artStr = art.getArtifactValue();

      try {
         IDPCache.responsesByArtifacts().put(artStr, res);
         if (SAML2Utils.isSAML2FailOverEnabled()) {
            long expireTime = getValidTimeofResponse(realm, idpEntityID, res);
            SAML2Repository.getInstance().save(artStr, res.toXMLString(true, true), expireTime / 1000L, null);
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message(classMethod + "Save Response to DB!");
            }
         }

         String messageEncoding = SAML2Utils.getAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "responseArtifactMessageEncoding");
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message(classMethod + "messageEncoding = " + messageEncoding);
            SAML2Utils.DEBUG.message(classMethod + "artStr = " + artStr);
         }

         if (messageEncoding != null && messageEncoding.equals("FORM")) {
            String[] logdata = new String[]{idpEntityID, realm, acsURL};
            LogUtil.access(Level.INFO, "SEND_ARTIFACT", logdata, session, props);
            SAML2Utils.postToTarget(request, response, "SAMLart", artStr, "RelayState", relayState, acsURL);
         } else {
            String redirectURL = acsURL + (acsURL.contains("?") ? "&" : "?") + "SAMLart=" + URLEncDec.encode(artStr);
            if (relayState != null && !relayState.trim().isEmpty()) {
               redirectURL = redirectURL + "&RelayState=" + URLEncDec.encode(relayState);
            }

            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message(classMethod + "Redirect URL = " + redirectURL);
            }

            String[] logdata = new String[]{idpEntityID, realm, redirectURL};
            LogUtil.access(Level.INFO, "SEND_ARTIFACT", logdata, session, props);
            response.sendRedirect(redirectURL);
         }
      } catch (IOException ioe) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to send redirect: ", ioe);
      } catch (Exception e) {
         SAML2Utils.DEBUG.error(classMethod + "DB Error!", e);
      }
   }

   private static void sendResponseECP(HttpServletResponse response, String idpEntityID, String realm, String acsURL, Response res) throws SAML2Exception {
      ECPFactory ecpFactory = ECPFactory.getInstance();
      ECPResponse ecpResponse = ecpFactory.createECPResponse();
      ecpResponse.setMustUnderstand(Boolean.TRUE);
      ecpResponse.setActor("http://schemas.xmlsoap.org/soap/actor/next");
      ecpResponse.setAssertionConsumerServiceURL(acsURL);
      String header = ecpResponse.toXMLString(true, true);
      String body = res.toXMLString(true, true);

      try {
         SOAPMessage reply = SAML2Utils.createSOAPMessage(header, body, false);
         String[] logdata = new String[]{idpEntityID, realm, acsURL, ""};
         if (LogUtil.isAccessLoggable(Level.FINE)) {
            logdata[3] = SAML2Utils.soapMessageToString(reply);
         }

         LogUtil.access(Level.INFO, "SEND_ECP_RESPONSE", logdata, null);
         if (reply.saveRequired()) {
            reply.saveChanges();
         }

         response.setStatus(200);
         SAML2Utils.putHeaders(reply.getMimeHeaders(), response);
         ByteArrayOutputStream buffer = new ByteArrayOutputStream(8192);
         reply.writeTo(buffer);
         Saml2ResponseBodyWriter.writeBodyWithoutCommittingResponse(buffer, response);
      } catch (Exception ex) {
         SAML2Utils.DEBUG.error("IDPSSOUtil.sendResponseECP", ex);
         String[] data = new String[]{idpEntityID, realm, acsURL};
         LogUtil.error(Level.INFO, "SEND_ECP_RESPONSE_FAILED", data, null);
         SAMLUtils.sendError(response, "failedToSendECPResponse", ex.getMessage());
      }
   }

   public static String getSessionIndex(Object session) {
      String classMethod = "IDPSSOUtil.getSessionIndex: ";
      if (session == null) {
         return null;
      }

      String[] values = null;

      try {
         values = sessionProvider.getProperty(session, "SAML2IDPSessionIndex");
      } catch (SessionException e) {
         SAML2Utils.DEBUG.error(classMethod + "error retrieving session index from the session: ", e);
         values = null;
      }

      if (values != null && values.length != 0) {
         String index = values[0];
         if (index != null && !index.isEmpty()) {
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message(classMethod + "Returning sessionIndex=" + index);
            }

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

   private static String getAttributeValueFromIDPSSOConfig(String realm, String hostEntityId, String attrName) {
      String classMethod = "IDPSSOUtil.getAttributeValueFromIDPSSOConfig: ";
      String result = null;

      try {
         BaseConfigType config = META_MANAGER.getIDPSSOConfig(realm, hostEntityId);
         Map<String, List<String>> attrs = SAML2MetaUtils.getAttributes(config);
         List<String> value = attrs.get(attrName);
         if (value != null && !value.isEmpty()) {
            result = value.getFirst();
         }
      } catch (SAML2MetaException sme) {
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message(classMethod + "get IDPSSOConfig failed:", sme);
         }

         result = null;
      }

      return result;
   }

   private static void redirectAuthentication(HttpServletResponse response, String reqID) throws SAML2Exception, IOException {
      IDPSSOFederate.redirectToFlowAuthUri(response, reqID);
   }

   private static void signAssertion(String realm, String idpEntityID, Assertion assertion) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.signAssertion: ";
      KeyProvider kp = KeyUtil.getKeyProviderInstance();
      if (kp == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get a key provider instance.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullKeyProvider"));
      }

      String idpSignCertAlias = SAML2Utils.getSigningCertAlias(realm, idpEntityID, "IDPRole");
      if (idpSignCertAlias == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get the hosted IDP signing certificate alias.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("missingSigningCertAlias"));
      }

      assertion.sign(kp.getPrivateKey(idpSignCertAlias), kp.getX509Certificate(idpSignCertAlias));
   }

   static void signAndEncryptResponseComponents(String realm, String spEntityID, String idpEntityID, Response res, boolean signAssertion) throws SAML2Exception {
      String classMethod = "IDPSSOUtil.signAndEncryptResponseComponents: ";
      boolean toEncryptAssertion = false;
      boolean toEncryptNameID = false;
      boolean toEncryptAttribute = false;
      if (res != null) {
         List<Assertion> assertions = res.getAssertion();
         if (assertions != null && !assertions.isEmpty()) {
            Assertion assertion = assertions.getFirst();
            String wantAssertionEncrypted = SAML2Utils.getAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "wantAssertionEncrypted");
            toEncryptAssertion = wantAssertionEncrypted != null && wantAssertionEncrypted.equals("true");
            if (!toEncryptAssertion) {
               String wantNameIDEncrypted = SAML2Utils.getAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "wantNameIDEncrypted");
               toEncryptNameID = wantNameIDEncrypted != null && wantNameIDEncrypted.equals("true");
               String wantAttributeEncrypted = SAML2Utils.getAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "wantAttributeEncrypted");
               toEncryptAttribute = wantAttributeEncrypted != null && wantAttributeEncrypted.equals("true");
            }

            if (!toEncryptAssertion && !toEncryptNameID && !toEncryptAttribute) {
               if (signAssertion) {
                  signAssertion(realm, idpEntityID, assertion);
                  List<Assertion> assertionList = new ArrayList<>();
                  assertionList.add(assertion);
                  res.setAssertion(assertionList);
               }
            } else {
               SPSSODescriptorType spSSODescriptorElement = null;
               if (META_MANAGER == null) {
                  SAML2Utils.DEBUG.error(classMethod + "Unable to get meta manager.");
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorMetaManager"));
               }

               try {
                  spSSODescriptorElement = META_MANAGER.getSPSSODescriptor(realm, spEntityID);
                  if (spSSODescriptorElement == null) {
                     SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
                     String[] data = new String[]{spEntityID};
                     LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
                     throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
                  }
               } catch (SAML2MetaException sme) {
                  SAML2Utils.DEBUG.error(classMethod + "Unable to get SP SSO Descriptor from meta.");
                  String[] data = new String[]{spEntityID};
                  LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
               }

               EncInfo encInfo = KeyUtil.getEncInfo(spSSODescriptorElement, spEntityID, "SPRole");
               if (encInfo == null) {
                  SAML2Utils.DEBUG.error(classMethod + "failed to get service provider encryption key info.");
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("UnableToFindEncryptKeyInfo"));
               }

               if (toEncryptAssertion) {
                  if (signAssertion) {
                     signAssertion(realm, idpEntityID, assertion);
                  }

                  EncryptedAssertion encryptedAssertion = assertion.encrypt(
                     encInfo.getWrappingKey(), encInfo.getDataEncAlgorithm(), encInfo.getDataEncStrength(), spEntityID
                  );
                  if (encryptedAssertion == null) {
                     SAML2Utils.DEBUG.error(classMethod + "failed to encrypt the assertion.");
                     throw new SAML2Exception(SAML2Utils.BUNDLE.getString("FailedToEncryptAssertion"));
                  }

                  List<EncryptedAssertion> assertionList = new ArrayList<>();
                  assertionList.add(encryptedAssertion);
                  res.setEncryptedAssertion(assertionList);
                  res.setAssertion(new ArrayList<>());
                  if (SAML2Utils.DEBUG.messageEnabled()) {
                     SAML2Utils.DEBUG.message(classMethod + "Assertion encrypted.");
                  }
               } else {
                  if (toEncryptNameID) {
                     Subject subject = assertion.getSubject();
                     if (subject == null) {
                        return;
                     }

                     NameID nameID = subject.getNameID();
                     if (nameID == null) {
                        return;
                     }

                     EncryptedID encryptedNameID = nameID.encrypt(
                        encInfo.getWrappingKey(), encInfo.getDataEncAlgorithm(), encInfo.getDataEncStrength(), spEntityID
                     );
                     if (encryptedNameID == null) {
                        SAML2Utils.DEBUG.error(classMethod + "failed to encrypt the NameID.");
                        throw new SAML2Exception(SAML2Utils.BUNDLE.getString("FailedToEncryptNameID"));
                     }

                     subject.setEncryptedID(encryptedNameID);
                     subject.setNameID(null);
                     assertion.setSubject(subject);
                     if (SAML2Utils.DEBUG.messageEnabled()) {
                        SAML2Utils.DEBUG.message(classMethod + "NameID encrypted.");
                     }
                  }

                  if (toEncryptAttribute) {
                     List<AttributeStatement> attributeStatements = assertion.getAttributeStatements();
                     if (attributeStatements != null && !attributeStatements.isEmpty()) {
                        List<AttributeStatement> stmts = new ArrayList<>();

                        for (AttributeStatement attributeStatement : attributeStatements) {
                           List<Attribute> attributes = attributeStatement.getAttribute();
                           if (attributes != null && !attributes.isEmpty()) {
                              List<EncryptedAttribute> eaList = new ArrayList<>();

                              for (Attribute attribute : attributes) {
                                 EncryptedAttribute encryptedAttribute = attribute.encrypt(
                                    encInfo.getWrappingKey(), encInfo.getDataEncAlgorithm(), encInfo.getDataEncStrength(), spEntityID
                                 );
                                 if (encryptedAttribute == null) {
                                    SAML2Utils.DEBUG.error(classMethod + "failed to encrypt the Attribute.");
                                    throw new SAML2Exception(SAML2Utils.BUNDLE.getString("FailedToEncryptAttribute"));
                                 }

                                 eaList.add(encryptedAttribute);
                              }

                              attributeStatement.setEncryptedAttribute(eaList);
                              attributeStatement.setAttribute(new ArrayList<>());
                              stmts.add(attributeStatement);
                           }
                        }

                        assertion.setAttributeStatements(stmts);
                        if (SAML2Utils.DEBUG.messageEnabled()) {
                           SAML2Utils.DEBUG.message(classMethod + "Attribute encrypted.");
                        }
                     }
                  }

                  if (signAssertion) {
                     signAssertion(realm, idpEntityID, assertion);
                  }

                  List<Assertion> assertionList = new ArrayList<>();
                  assertionList.add(assertion);
                  res.setAssertion(assertionList);
               }
            }
         }
      }
   }

   private static int getEffectiveTime(String realm, String idpEntityID) {
      int effectiveTime = 600;
      String effectiveTimeStr = getAttributeValueFromIDPSSOConfig(realm, idpEntityID, "assertionEffectiveTime");
      if (effectiveTimeStr != null) {
         try {
            effectiveTime = Integer.parseInt(effectiveTimeStr);
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("IDPSSOUtil.getEffectiveTime: got effective time from config:" + effectiveTime);
            }
         } catch (NumberFormatException nfe) {
            SAML2Utils.DEBUG.error("IDPSSOUtil.getEffectiveTime: Failed to get assertion effective time from IDP SSO config: ", nfe);
            effectiveTime = 600;
         }
      }

      return effectiveTime;
   }

   private static int getNotBeforeSkewTime(String realm, String idpEntityID) {
      String classMethod = "IDPSSOUtil.getNotBeforeSkewTime:";
      int notBeforeSkewTime = 600;
      String skewTimeStr = getAttributeValueFromIDPSSOConfig(realm, idpEntityID, "assertionNotBeforeTimeSkew");
      if (skewTimeStr != null) {
         try {
            notBeforeSkewTime = Integer.parseInt(skewTimeStr);
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message(classMethod + "got NotBefore skew time from config:" + notBeforeSkewTime);
            }
         } catch (NumberFormatException nfe) {
            SAML2Utils.DEBUG.error(classMethod + "IDP SSO config: ", nfe);
            notBeforeSkewTime = 600;
         }
      }

      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message(classMethod + "NotBefore Skew time :" + notBeforeSkewTime);
      }

      return notBeforeSkewTime;
   }

   private static long getValidTimeofResponse(String realm, String idpEntityID, Response response) throws SAML2Exception {
      int timeskew = 300;
      String timeskewStr = getAttributeValueFromIDPSSOConfig(realm, idpEntityID, "assertionTimeSkew");
      if (timeskewStr != null && !timeskewStr.trim().isEmpty()) {
         timeskew = Integer.parseInt(timeskewStr);
         if (timeskew < 0) {
            timeskew = 300;
         }
      }

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

      List<Assertion> assertions = response.getAssertion();
      if (assertions != null && !assertions.isEmpty()) {
         Assertion assertion = assertions.getFirst();
         Conditions cond = assertion.getConditions();
         if (cond == null) {
            throw new SAML2Exception("nullConditions");
         }

         Date notOnOrAfter = cond.getNotOnOrAfter();
         long ret = notOnOrAfter.getTime() + timeskew * 1000L;
         if (notOnOrAfter != null && ret >= System.currentTimeMillis()) {
            return ret;
         }

         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message("Time in Assertion  is invalid.");
         }

         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidTimeOnResponse"));
      } else {
         return System.currentTimeMillis() + getEffectiveTime(realm, idpEntityID) + timeskew * 1000L;
      }
   }

   private static void signResponse(String realm, String idpEntityID, Response response) throws SAML2Exception {
      String classMethod = "IDPSSOUtil:signResponse";
      KeyProvider kp = KeyUtil.getKeyProviderInstance();
      if (kp == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get a key provider instance.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullKeyProvider"));
      }

      String idpSignCertAlias = SAML2Utils.getSigningCertAlias(realm, idpEntityID, "IDPRole");
      if (idpSignCertAlias == null) {
         SAML2Utils.DEBUG.error(classMethod + "Unable to get the hosted IDP signing certificate alias.");
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("missingSigningCertAlias"));
      }

      response.sign(kp.getPrivateKey(idpSignCertAlias), kp.getX509Certificate(idpSignCertAlias));
   }

   static {
      try {
         sessionProvider = SessionManager.getProvider();
      } catch (SessionException se) {
         SAML2Utils.DEBUG.error("IDPSSOUtil static block: Error getting SessionProvider.", se);
      }
   }
}
