@Audit(
      action = "SERVICE_TICKET",
      actionResolverName = "GRANT_SERVICE_TICKET_RESOLVER",
      resourceResolverName = "GRANT_SERVICE_TICKET_RESOURCE_RESOLVER")
  @Profiled(tag = "GRANT_SERVICE_TICKET", logFailuresSeparately = false)
  @Transactional(readOnly = false)
  @Override
  public String grantServiceTicket(
      final String ticketGrantingTicketId, final Service service, final Credential... credentials)
      throws AuthenticationException, TicketException {
    Assert.notNull(ticketGrantingTicketId, "ticketGrantingticketId cannot be null");
    Assert.notNull(service, "service cannot be null");

    final TicketGrantingTicket ticketGrantingTicket =
        this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);

    if (ticketGrantingTicket == null) {
      logger.debug(
          "TicketGrantingTicket [{}] cannot be found in the ticket registry.",
          ticketGrantingTicketId);
      throw new InvalidTicketException(ticketGrantingTicketId);
    }

    synchronized (ticketGrantingTicket) {
      if (ticketGrantingTicket.isExpired()) {
        this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
        logger.debug(
            "TicketGrantingTicket[{}] has expired and is now deleted from the ticket registry.",
            ticketGrantingTicketId);
        throw new InvalidTicketException(ticketGrantingTicketId);
      }
    }

    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

    verifyRegisteredServiceProperties(registeredService, service);

    if (!registeredService.isSsoEnabled()
        && credentials == null
        && ticketGrantingTicket.getCountOfUses() > 0) {
      logger.warn("ServiceManagement: Service [{}] is not allowed to use SSO.", service.getId());
      throw new UnauthorizedSsoServiceException();
    }

    // CAS-1019
    final List<Authentication> authns = ticketGrantingTicket.getChainedAuthentications();
    if (authns.size() > 1) {
      if (!registeredService.isAllowedToProxy()) {
        final String message =
            String.format(
                "ServiceManagement: Proxy attempt by service [%s] (registered service [%s]) is not allowed.",
                service.getId(), registeredService.toString());
        logger.warn(message);
        throw new UnauthorizedProxyingException(message);
      }
    }

    if (credentials != null) {
      final Authentication current = this.authenticationManager.authenticate(credentials);
      final Authentication original = ticketGrantingTicket.getAuthentication();
      if (!current.getPrincipal().equals(original.getPrincipal())) {
        throw new MixedPrincipalException(current, current.getPrincipal(), original.getPrincipal());
      }
      ticketGrantingTicket.getSupplementalAuthentications().add(current);
    }

    // Perform security policy check by getting the authentication that satisfies the configured
    // policy
    // This throws if no suitable policy is found
    getAuthenticationSatisfiedByPolicy(
        ticketGrantingTicket, new ServiceContext(service, registeredService));

    final String uniqueTicketIdGenKey = service.getClass().getName();
    if (!this.uniqueTicketIdGeneratorsForService.containsKey(uniqueTicketIdGenKey)) {
      logger.warn(
          "Cannot create service ticket because the key [{}] for service [{}] is not linked to a ticket id generator",
          uniqueTicketIdGenKey,
          service.getId());
      throw new UnauthorizedSsoServiceException();
    }

    final UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator =
        this.uniqueTicketIdGeneratorsForService.get(uniqueTicketIdGenKey);

    final String generatedServiceTicketId =
        serviceTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX);
    logger.debug(
        "Generated service ticket id [{}] for ticket granting ticket [{}]",
        generatedServiceTicketId,
        ticketGrantingTicket.getId());

    final ServiceTicket serviceTicket =
        ticketGrantingTicket.grantServiceTicket(
            generatedServiceTicketId,
            service,
            this.serviceTicketExpirationPolicy,
            credentials != null);

    this.serviceTicketRegistry.addTicket(serviceTicket);

    if (logger.isInfoEnabled()) {
      final List<Authentication> authentications =
          serviceTicket.getGrantingTicket().getChainedAuthentications();
      final String formatString = "Granted %s ticket [%s] for service [%s] for user [%s]";
      final String type;
      final String principalId =
          authentications.get(authentications.size() - 1).getPrincipal().getId();

      if (authentications.size() == 1) {
        type = "service";
      } else {
        type = "proxy";
      }

      logger.info(
          String.format(formatString, type, serviceTicket.getId(), service.getId(), principalId));
    }

    return serviceTicket.getId();
  }