/**
   * Updates the user's Shibboleth session with authentication information. If no session exists a
   * new one will be created.
   *
   * @param loginContext current login context
   * @param authenticationSubject subject created from the authentication method
   * @param authenticationMethod the method used to authenticate the subject
   * @param authenticationInstant the time of authentication
   * @param httpRequest current HTTP request
   * @param httpResponse current HTTP response
   */
  protected void updateUserSession(
      LoginContext loginContext,
      Subject authenticationSubject,
      String authenticationMethod,
      DateTime authenticationInstant,
      HttpServletRequest httpRequest,
      HttpServletResponse httpResponse) {
    Principal authenticationPrincipal = authenticationSubject.getPrincipals().iterator().next();
    LOG.debug("Updating session information for principal {}", authenticationPrincipal.getName());

    Session idpSession = (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    if (idpSession == null) {
      LOG.debug("Creating shibboleth session for principal {}", authenticationPrincipal.getName());
      idpSession = (Session) sessionManager.createSession();
      loginContext.setSessionID(idpSession.getSessionID());
      addSessionCookie(httpRequest, httpResponse, idpSession);
    }

    // Merge the information in the current session subject with the information from the
    // login handler subject
    idpSession.setSubject(mergeSubjects(idpSession.getSubject(), authenticationSubject));

    // Check if an existing authentication method with no updated timestamp was used (i.e. SSO
    // occurred);
    // if not record the new information
    AuthenticationMethodInformation authnMethodInfo =
        idpSession.getAuthenticationMethods().get(authenticationMethod);
    if (authnMethodInfo == null || authenticationInstant != null) {
      LOG.debug(
          "Recording authentication and service information in Shibboleth session for principal: {}",
          authenticationPrincipal.getName());
      LoginHandler loginHandler =
          handlerManager.getLoginHandlers().get(loginContext.getAttemptedAuthnMethod());
      DateTime authnInstant = authenticationInstant;
      if (authnInstant == null) {
        authnInstant = new DateTime();
      }
      authnMethodInfo =
          new AuthenticationMethodInformationImpl(
              idpSession.getSubject(),
              authenticationPrincipal,
              authenticationMethod,
              authnInstant,
              loginHandler.getAuthenticationDuration());
    }

    loginContext.setAuthenticationMethodInformation(authnMethodInfo);
    idpSession
        .getAuthenticationMethods()
        .put(authnMethodInfo.getAuthenticationMethod(), authnMethodInfo);
    sessionManager.indexSession(idpSession, idpSession.getPrincipalName());

    ServiceInformation serviceInfo =
        new ServiceInformationImpl(
            loginContext.getRelyingPartyId(), new DateTime(), authnMethodInfo);
    idpSession.getServicesInformation().put(serviceInfo.getEntityID(), serviceInfo);
  }
  /**
   * Filters out any login handler based on the requirement for forced authentication.
   *
   * <p>During forced authentication any handler that has not previously been used to authenticate
   * the user or any handlers that have been and support force re-authentication may be used. Filter
   * out any of the other ones.
   *
   * @param idpSession user's current IdP session
   * @param loginContext current login context
   * @param loginHandlers login handlers to filter
   * @throws ForceAuthenticationException thrown if no handlers remain after filtering
   */
  protected void filterByForceAuthentication(
      Session idpSession, LoginContext loginContext, Map<String, LoginHandler> loginHandlers)
      throws ForceAuthenticationException {
    LOG.debug("Forced authentication is required, filtering possible login handlers accordingly");

    ArrayList<AuthenticationMethodInformation> activeMethods =
        new ArrayList<AuthenticationMethodInformation>();
    if (idpSession != null) {
      activeMethods.addAll(idpSession.getAuthenticationMethods().values());
    }

    loginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);

    LoginHandler loginHandler;
    for (AuthenticationMethodInformation activeMethod : activeMethods) {
      loginHandler = loginHandlers.get(activeMethod.getAuthenticationMethod());
      if (loginHandler != null && !loginHandler.supportsForceAuthentication()) {
        for (String handlerSupportedMethods : loginHandler.getSupportedAuthenticationMethods()) {
          LOG.debug(
              "Removing LoginHandler {}, it does not support forced re-authentication",
              loginHandler.getClass().getName());
          loginHandlers.remove(handlerSupportedMethods);
        }
      }
    }

    LOG.debug(
        "Authentication handlers remaining after forced authentication requirement filtering: {}",
        loginHandlers);

    if (loginHandlers.isEmpty()) {
      LOG.info("Force authentication requested but no login handlers available to support it");
      throw new ForceAuthenticationException();
    }
  }
  /**
   * Filters out the previous session login handler if there is no existing IdP session, no active
   * authentication methods, or if at least one of the active authentication methods do not match
   * the requested authentication methods.
   *
   * @param supportedLoginHandlers login handlers supported by the authentication engine for this
   *     request, never null
   * @param idpSession current IdP session, may be null if no session currently exists
   * @param loginContext current login context, never null
   */
  protected void filterPreviousSessionLoginHandler(
      Map<String, LoginHandler> supportedLoginHandlers,
      Session idpSession,
      LoginContext loginContext) {
    if (!supportedLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
      return;
    }

    if (idpSession == null) {
      LOG.debug(
          "Filtering out previous session login handler because there is no existing IdP session");
      supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
      return;
    }
    Collection<AuthenticationMethodInformation> currentAuthnMethods =
        idpSession.getAuthenticationMethods().values();

    Iterator<AuthenticationMethodInformation> methodItr = currentAuthnMethods.iterator();
    while (methodItr.hasNext()) {
      AuthenticationMethodInformation info = methodItr.next();
      if (info.isExpired()) {
        methodItr.remove();
      }
    }
    if (currentAuthnMethods.isEmpty()) {
      LOG.debug(
          "Filtering out previous session login handler because there are no active authentication methods");
      supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
      return;
    }

    List<String> requestedMethods = loginContext.getRequestedAuthenticationMethods();
    if (requestedMethods != null && !requestedMethods.isEmpty()) {
      boolean retainPreviousSession = false;
      for (AuthenticationMethodInformation currentAuthnMethod : currentAuthnMethods) {
        if (loginContext
            .getRequestedAuthenticationMethods()
            .contains(currentAuthnMethod.getAuthenticationMethod())) {
          retainPreviousSession = true;
          break;
        }
      }

      if (!retainPreviousSession) {
        LOG.debug(
            "Filtering out previous session login handler, no active authentication methods match required methods");
        supportedLoginHandlers.remove(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);
        return;
      }
    }
  }
  /**
   * Selects a login handler from a list of possible login handlers that could be used for the
   * request.
   *
   * @param possibleLoginHandlers list of possible login handlers that could be used for the request
   * @param loginContext current login context
   * @param idpSession current IdP session, if one exists
   * @return the login handler to use for this request
   * @throws AuthenticationException thrown if no handler can be used for this request
   */
  protected LoginHandler selectLoginHandler(
      Map<String, LoginHandler> possibleLoginHandlers,
      LoginContext loginContext,
      Session idpSession)
      throws AuthenticationException {
    LOG.debug("Selecting appropriate login handler from filtered set {}", possibleLoginHandlers);
    LoginHandler loginHandler;
    if (idpSession != null
        && possibleLoginHandlers.containsKey(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX)) {
      LOG.debug("Authenticating user with previous session LoginHandler");
      loginHandler = possibleLoginHandlers.get(AuthnContext.PREVIOUS_SESSION_AUTHN_CTX);

      for (AuthenticationMethodInformation authnMethod :
          idpSession.getAuthenticationMethods().values()) {
        if (authnMethod.isExpired()) {
          continue;
        }

        if (loginContext.getRequestedAuthenticationMethods().isEmpty()
            || loginContext
                .getRequestedAuthenticationMethods()
                .contains(authnMethod.getAuthenticationMethod())) {
          LOG.debug(
              "Basing previous session authentication on active authentication method {}",
              authnMethod.getAuthenticationMethod());
          loginContext.setAttemptedAuthnMethod(authnMethod.getAuthenticationMethod());
          loginContext.setAuthenticationMethodInformation(authnMethod);
          return loginHandler;
        }
      }
    }

    if (loginContext.getDefaultAuthenticationMethod() != null
        && possibleLoginHandlers.containsKey(loginContext.getDefaultAuthenticationMethod())) {
      loginHandler = possibleLoginHandlers.get(loginContext.getDefaultAuthenticationMethod());
      loginContext.setAttemptedAuthnMethod(loginContext.getDefaultAuthenticationMethod());
    } else {
      Entry<String, LoginHandler> chosenLoginHandler =
          possibleLoginHandlers.entrySet().iterator().next();
      loginContext.setAttemptedAuthnMethod(chosenLoginHandler.getKey());
      loginHandler = chosenLoginHandler.getValue();
    }

    LOG.debug(
        "Authenticating user with login handler of type {}", loginHandler.getClass().getName());
    return loginHandler;
  }
  /**
   * If forced authentication was required this method checks to ensure that the re-authenticated
   * subject contains a principal name that is equal to the principal name associated with the
   * authentication method. If this is the first time the subject has authenticated with this method
   * than this check always passes.
   *
   * @param idpSession user's IdP session
   * @param authnMethod method used to authenticate the user
   * @param subject subject that was authenticated
   * @throws AuthenticationException thrown if this check fails
   */
  protected void validateForcedReauthentication(
      Session idpSession, String authnMethod, Subject subject) throws AuthenticationException {
    if (idpSession != null) {
      AuthenticationMethodInformation authnMethodInfo =
          idpSession.getAuthenticationMethods().get(authnMethod);
      if (authnMethodInfo != null) {
        boolean princpalMatch = false;
        for (Principal princpal : subject.getPrincipals()) {
          if (authnMethodInfo.getAuthenticationPrincipal().equals(princpal)) {
            princpalMatch = true;
            break;
          }
        }

        if (!princpalMatch) {
          throw new ForceAuthenticationException(
              "Authenticated principal does not match previously authenticated principal");
        }
      }
    }
  }