/**
   * Gets mfa request context.
   *
   * @param serviceMfaData service specific mfa settings
   * @param attributeValue the value found in the attribute
   * @param targetService the target service
   * @return the mfa request context
   */
  private MultiFactorAuthenticationRequestContext getMfaRequestContext(
      final ServiceMfaData serviceMfaData,
      final String attributeValue,
      final WebApplicationService targetService) {
    final RegisteredService registeredService = this.servicesManager.findServiceBy(targetService);
    final RegisteredServiceWithAttributes service =
        RegisteredServiceWithAttributes.class.cast(registeredService);
    final String method = String.class.cast(service.getExtraAttributes().get("method"));

    if (match(serviceMfaData.getAttributePattern(), attributeValue)) {
      if (!this.authenticationMethodConfiguration.containsAuthenticationMethod(
          serviceMfaData.getAuthenticationMethod())) {
        logger.info(
            "MFA attribute [{}] with value [{}] is not supported by the authentication method configuration.",
            serviceMfaData.getAttributeName(),
            serviceMfaData.getAuthenticationMethod());
        return null;
      }
      final int mfaMethodRank =
          this.authenticationMethodConfiguration
              .getAuthenticationMethod(serviceMfaData.getAuthenticationMethod())
              .getRank();
      final MultiFactorAuthenticationSupportingWebApplicationService svc =
          this.mfaServiceFactory.create(
              targetService.getId(),
              targetService.getId(),
              targetService.getArtifactId(),
              "POST".equals(method) ? ResponseType.POST : ResponseType.REDIRECT,
              serviceMfaData.getAuthenticationMethod(),
              MultiFactorAuthenticationSupportingWebApplicationService.AuthenticationMethodSource
                  .PRINCIPAL_ATTRIBUTE);

      return new MultiFactorAuthenticationRequestContext(svc, mfaMethodRank);
    }

    logger.trace("{} did not match {}", attributeValue, serviceMfaData.getAttributePattern());
    return null;
  }
  @Override
  public final WebApplicationService extractService(final HttpServletRequest request) {
    final WebApplicationService targetService = getTargetService(request);
    if (targetService == null) {
      return null;
    }
    String authenticationMethod = this.getAuthenticationMethod(request, targetService);
    if (StringUtils.isBlank(authenticationMethod)) {
      return null;
    }
    authenticationMethod =
        this.authenticationMethodTranslator.translate(targetService, authenticationMethod);
    this.authenticationMethodVerifier.verifyAuthenticationMethod(
        authenticationMethod, targetService, request);

    // Grab the HTTP method for the response off of the request.
    final String method = request.getParameter(CONST_PARAM_METHOD);

    final MultiFactorAuthenticationSupportingWebApplicationService mfaService =
        this.mfaWebApplicationServiceFactory.create(
            targetService.getId(),
            targetService.getId(),
            targetService.getArtifactId(),
            "POST".equalsIgnoreCase(method) ? ResponseType.POST : ResponseType.REDIRECT,
            authenticationMethod,
            getAuthenticationMethodSource());

    logger.debug(
        "Created multifactor authentication service instance for [{}] with [{}] as [{}] "
            + "and authentication method definition source [{}].",
        mfaService.getId(),
        CONST_PARAM_AUTHN_METHOD,
        mfaService.getAuthenticationMethod(),
        mfaService.getAuthenticationMethodSource());

    return mfaService;
  }
  /**
   * Handle the request. Specially, abides by the default behavior specified in the {@link
   * org.jasig.cas.web.ServiceValidateController} and then, invokes the {@link #getCommandClass()}
   * method to delegate the task of spec validation.
   *
   * @param request request object
   * @param response response object
   * @return A {@link ModelAndView} object pointing to either {@link #setSuccessView(String)} or
   *     {@link #setFailureView(String)}
   * @throws Exception In case the authentication method cannot be retrieved by the binder from the
   *     incoming request.
   */
  @Override
  protected final ModelAndView handleRequestInternal(
      final HttpServletRequest request, final HttpServletResponse response) throws Exception {
    final WebApplicationService service = this.argumentExtractor.extractService(request);
    final String serviceTicketId = service != null ? service.getArtifactId() : null;
    final String authnMethod = getAuthenticationMethodFromRequest(request);

    if (service == null || serviceTicketId == null) {
      logger.debug(
          String.format(
              "Could not process request; Service: %s, Service Ticket Id: %s",
              service, serviceTicketId));
      return generateErrorView("INVALID_REQUEST", "INVALID_REQUEST", authnMethod, null);
    }

    try {
      final Credential serviceCredentials = getServiceCredentialsFromRequest(request);
      String proxyGrantingTicketId = null;

      if (serviceCredentials != null) {
        try {
          proxyGrantingTicketId =
              this.centralAuthenticationService.delegateTicketGrantingTicket(
                  serviceTicketId, serviceCredentials);
        } catch (final TicketException e) {
          logger.error("TicketException generating ticket for: " + serviceCredentials, e);
        }
      }

      final Assertion assertion =
          this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
      final AbstractMultiFactorAuthenticationProtocolValidationSpecification
          validationSpecification = this.getCommandClass();
      final ServletRequestDataBinder binder =
          new ServletRequestDataBinder(validationSpecification, "validationSpecification");
      initBinder(request, binder);
      binder.bind(request);

      /**
       * The binder does not support field aliases. This means that the request parameter names must
       * exactly match the validation spec fields, or the match fails. Since the validation request
       * per the modified protocol will use 'authn_method', we could either create a matching field
       * inside the validation object, create a custom data binder object that does the conversion,
       * or simply bind the parameter manually.
       *
       * <p>This implementation opts for the latter choice.
       */
      validationSpecification.setAuthenticationMethod(authnMethod);

      try {
        if (!validationSpecification.isSatisfiedBy(assertion)) {
          logger.debug(
              "ServiceTicket [" + serviceTicketId + "] does not satisfy validation specification.");
          return generateErrorView("INVALID_TICKET", "INVALID_TICKET_SPEC", authnMethod, null);
        }
      } catch (final UnrecognizedMultiFactorAuthenticationMethodException e) {
        logger.debug(e.getMessage(), e);
        return generateErrorView(
            e.getCode(), e.getMessage(), authnMethod, new Object[] {e.getAuthenticationMethod()});
      } catch (final UnacceptableMultiFactorAuthenticationMethodException e) {
        logger.debug(e.getMessage(), e);
        return generateErrorView(
            e.getCode(),
            e.getMessage(),
            authnMethod,
            new Object[] {serviceTicketId, e.getAuthenticationMethod()});
      }

      onSuccessfulValidation(serviceTicketId, assertion);

      final ModelAndView success = new ModelAndView(this.successView);
      success.addObject(MODEL_ASSERTION, assertion);

      if (serviceCredentials != null && proxyGrantingTicketId != null) {
        final String proxyIou = this.proxyHandler.handle(serviceCredentials, proxyGrantingTicketId);
        success.addObject(MODEL_PROXY_GRANTING_TICKET_IOU, proxyIou);
      }

      final String authnMethods =
          MultiFactorUtils.getFulfilledAuthenticationMethodsAsString(assertion);
      if (StringUtils.isNotBlank(authnMethods)) {
        success.addObject(MODEL_AUTHN_METHOD, authnMethods);
      }
      logger.debug(String.format("Successfully validated service ticket: %s", serviceTicketId));

      return success;
    } catch (final TicketValidationException e) {
      return generateErrorView(
          e.getCode(),
          e.getCode(),
          authnMethod,
          new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()});
    } catch (final TicketException te) {
      return generateErrorView(
          te.getCode(), te.getCode(), authnMethod, new Object[] {serviceTicketId});
    } catch (final UnauthorizedServiceException e) {
      return generateErrorView(e.getMessage(), e.getMessage(), authnMethod, null);
    }
  }
  protected final ModelAndView handleRequestInternal(
      final HttpServletRequest request, final HttpServletResponse response) throws Exception {
    final WebApplicationService service = this.argumentExtractor.extractService(request);
    final String serviceTicketId = service != null ? service.getArtifactId() : null;

    if (service == null || serviceTicketId == null) {
      logger.debug(
          String.format(
              "Could not process request; Service: %s, Service Ticket Id: %s",
              service, serviceTicketId));
      return generateErrorView("INVALID_REQUEST", "INVALID_REQUEST", null);
    }

    try {
      final Credentials serviceCredentials = getServiceCredentialsFromRequest(request);
      String proxyGrantingTicketId = null;

      // XXX should be able to validate AND THEN use
      if (serviceCredentials != null) {
        try {
          proxyGrantingTicketId =
              this.centralAuthenticationService.delegateTicketGrantingTicket(
                  serviceTicketId, serviceCredentials);
        } catch (final TicketException e) {
          logger.error("TicketException generating ticket for: " + serviceCredentials, e);
        }
      }

      final Assertion assertion =
          this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);

      final ValidationSpecification validationSpecification = this.getCommandClass();
      final ServletRequestDataBinder binder =
          new ServletRequestDataBinder(validationSpecification, "validationSpecification");
      initBinder(request, binder);
      binder.bind(request);

      if (!validationSpecification.isSatisfiedBy(assertion)) {
        if (logger.isDebugEnabled()) {
          logger.debug(
              "ServiceTicket [" + serviceTicketId + "] does not satisfy validation specification.");
        }
        return generateErrorView("INVALID_TICKET", "INVALID_TICKET_SPEC", null);
      }

      onSuccessfulValidation(serviceTicketId, assertion);

      final ModelAndView success = new ModelAndView(this.successView);
      success.addObject(MODEL_ASSERTION, assertion);

      if (serviceCredentials != null && proxyGrantingTicketId != null) {
        final String proxyIou = this.proxyHandler.handle(serviceCredentials, proxyGrantingTicketId);
        success.addObject(MODEL_PROXY_GRANTING_TICKET_IOU, proxyIou);
      }

      if (logger.isDebugEnabled()) {
        logger.debug(
            String.format(
                "Successfully validated service ticket [%s] for service [%s]",
                serviceTicketId, service.getId()));
      }

      return success;
    } catch (final TicketValidationException e) {
      return generateErrorView(
          e.getCode(),
          e.getCode(),
          new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()});
    } catch (final TicketException te) {
      return generateErrorView(te.getCode(), te.getCode(), new Object[] {serviceTicketId});
    } catch (final UnauthorizedServiceException e) {
      return generateErrorView(e.getMessage(), e.getMessage(), null);
    }
  }