/**
   * 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;
      }
    }
  }
  /**
   * Adds an IdP session cookie to the outbound response.
   *
   * @param httpRequest current request
   * @param httpResponse current response
   * @param userSession user's session
   */
  protected void addSessionCookie(
      HttpServletRequest httpRequest, HttpServletResponse httpResponse, Session userSession) {
    httpRequest.setAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE, userSession);

    byte[] remoteAddress = httpRequest.getRemoteAddr().getBytes();
    byte[] sessionId = userSession.getSessionID().getBytes();

    String signature = null;
    try {
      MessageDigest digester = MessageDigest.getInstance("SHA");
      digester.update(userSession.getSessionSecret());
      digester.update(remoteAddress);
      digester.update(sessionId);
      signature = Base64.encodeBytes(digester.digest());
    } catch (GeneralSecurityException e) {
      LOG.error("Unable to compute signature over session cookie material", e);
    }

    LOG.debug("Adding IdP session cookie to HTTP response");
    StringBuilder cookieValue = new StringBuilder();
    cookieValue.append(Base64.encodeBytes(remoteAddress, Base64.DONT_BREAK_LINES)).append("|");
    cookieValue.append(Base64.encodeBytes(sessionId, Base64.DONT_BREAK_LINES)).append("|");
    cookieValue.append(signature);

    String cookieDomain = HttpServletHelper.getCookieDomain(context);

    Cookie sessionCookie =
        new Cookie(IDP_SESSION_COOKIE_NAME, HTTPTransportUtils.urlEncode(cookieValue.toString()));
    sessionCookie.setVersion(1);
    if (cookieDomain != null) {
      sessionCookie.setDomain(cookieDomain);
    }
    sessionCookie.setPath(
        "".equals(httpRequest.getContextPath()) ? "/" : httpRequest.getContextPath());
    sessionCookie.setSecure(httpRequest.isSecure());
    httpResponse.addCookie(sessionCookie);
  }
  /**
   * Begins the authentication process. Determines if forced re-authentication is required or if an
   * existing, active, authentication method is sufficient. Also determines, when authentication is
   * required, which handler to use depending on whether passive authentication is required.
   *
   * @param loginContext current login context
   * @param httpRequest current HTTP request
   * @param httpResponse current HTTP response
   */
  protected void startUserAuthentication(
      LoginContext loginContext, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
    LOG.debug("Beginning user authentication process.");
    try {
      Session idpSession =
          (Session) httpRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
      if (idpSession != null) {
        LOG.debug("Existing IdP session available for principal {}", idpSession.getPrincipalName());
      }

      Map<String, LoginHandler> possibleLoginHandlers =
          determinePossibleLoginHandlers(idpSession, loginContext);

      // Filter out possible candidate login handlers by forced and passive authentication
      // requirements
      if (loginContext.isForceAuthRequired()) {
        filterByForceAuthentication(idpSession, loginContext, possibleLoginHandlers);
      }

      if (loginContext.isPassiveAuthRequired()) {
        filterByPassiveAuthentication(idpSession, loginContext, possibleLoginHandlers);
      }

      LoginHandler loginHandler =
          selectLoginHandler(possibleLoginHandlers, loginContext, idpSession);
      loginContext.setAuthenticationAttempted();
      loginContext.setAuthenticationEngineURL(HttpHelper.getRequestUriWithoutContext(httpRequest));

      // Send the request to the login handler
      HttpServletHelper.bindLoginContext(
          loginContext, storageService, getServletContext(), httpRequest, httpResponse);
      loginHandler.login(httpRequest, httpResponse);
    } catch (AuthenticationException e) {
      loginContext.setAuthenticationFailure(e);
      returnToProfileHandler(httpRequest, httpResponse);
    }
  }
  /**
   * 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");
        }
      }
    }
  }